From 1feb34621e56531292df90f3732befa533ff9007 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Tue, 23 Jun 2026 09:32:18 -0700 Subject: [PATCH] video discover: cascade rail cards in instead of one block flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each rail showed a shimmer skeleton then revealed the whole row in a single fade — on a big library with hide-owned on (which pages deep server-side) that reads as a long blank then a pop. Cards now stagger in left-to-right via a per-card --i index (capped) + a 'backwards' fill-mode keyframe (so the entrance animation releases and :hover transforms still work). Applies to lazy rails (fillShelf) and the prepended personalized rows (staggerWithin). --- webui/static/video/video-discover.js | 23 +++++++++++++++++++++-- webui/static/video/video-side.css | 12 ++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/webui/static/video/video-discover.js b/webui/static/video/video-discover.js index e5ddfff3..53a815b4 100644 --- a/webui/static/video/video-discover.js +++ b/webui/static/video/video-discover.js @@ -402,6 +402,7 @@ ''; }).join(''); host.insertAdjacentHTML('afterbegin', html); + staggerWithin(host); hydrateGet(host); }) .catch(function () { /* personalization is best-effort */ }); @@ -429,6 +430,7 @@ ''; }).join(''); host.insertAdjacentHTML('afterbegin', html); + staggerWithin(host); hydrateGet(host); }) .catch(function () { /* gaps are best-effort */ }); @@ -453,6 +455,7 @@ '
' + items.map(card).join('') + '
' + ''; host.insertAdjacentHTML('afterbegin', html); + staggerWithin(host); hydrateGet(host); }) .catch(function () { /* best-effort */ }); @@ -504,6 +507,22 @@ var h = $('[data-vdsc-hideowned]'); return !!(h && h.checked); } + // Tag each card with its index so the CSS cascade reveals them one-by-one (not all at once). + // Capped so a long rail's tail doesn't lag too far behind the head. + function stagger(rail) { + if (!rail) return; + var c = rail.children; + for (var i = 0; i < c.length; i++) c[i].style.setProperty('--i', i < 14 ? i : 14); + } + // Stagger every not-yet-tagged filled rail under a host (used by the prepended + // personalized rows, which render their cards inline rather than via fillShelf). + function staggerWithin(host) { + if (!host) return; + var rails = host.querySelectorAll('.vdsc-shelf--in .vdsc-rail'); + for (var i = 0; i < rails.length; i++) { + if (!rails[i].getAttribute('data-stg')) { rails[i].setAttribute('data-stg', '1'); stagger(rails[i]); } + } + } function fillShelf(shelf) { if (!shelf || shelf.getAttribute('data-vdsc-loaded')) return; shelf.setAttribute('data-vdsc-loaded', '1'); @@ -515,8 +534,8 @@ .then(function (d) { var items = (d && d.items) || []; if (!items.length) { shelf.remove(); return; } // drop empty shelves - if (rail) { rail.innerHTML = items.map(card).join(''); hydrateGet(rail); } - shelf.classList.add('vdsc-shelf--in'); // reveal + if (rail) { rail.innerHTML = items.map(card).join(''); stagger(rail); hydrateGet(rail); } + shelf.classList.add('vdsc-shelf--in'); // reveal (cards cascade via --i) }) .catch(function () { shelf.remove(); }); } diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index e3889fb1..9f5c0c47 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -2449,15 +2449,19 @@ body[data-side="video"] #soulsync-toggle { display: none; } .vdsc-ig-empty-ic { font-size: 40px; margin-bottom: 10px; } .vdsc-ig-empty-sub { font-size: 12.5px; color: rgba(255, 255, 255, 0.38); margin-top: 6px; } -/* rails: a gentle fade-in when filled + per-title hue on hover */ -.vdsc-shelf--in .vdsc-rail { animation: vdscFade 0.45s ease both; } -@keyframes vdscFade { from { opacity: 0.35; } to { opacity: 1; } } +/* rails: cards cascade in (staggered by --i) when the row fills, instead of one block flash. + `backwards` fill holds the hidden start-state during the delay, then releases so :hover works. */ +.vdsc-shelf--in .vdsc-rail .vsr-card { + animation: vdscCardIn 0.42s cubic-bezier(0.22, 0.61, 0.36, 1) backwards; + animation-delay: calc(var(--i, 0) * 40ms); +} +@keyframes vdscCardIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } } .vdsc-rail .vsr-card, .vdsc-grid .vsr-card { transition: transform 0.2s ease, box-shadow 0.2s ease; } .vdsc-rail .vsr-card:hover, .vdsc-grid .vsr-card:hover { box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5), 0 0 26px -6px hsla(var(--vgm-h, 230), 80%, 58%, 0.5); } @media (prefers-reduced-motion: reduce) { - .vdsc-shelf--in .vdsc-rail { animation: none; } + .vdsc-shelf--in .vdsc-rail .vsr-card { animation: none; } .vdsc-amb { transition: none; } }