diff --git a/webui/static/video/video-discover.js b/webui/static/video/video-discover.js index 2213b747..7c9e65e8 100644 --- a/webui/static/video/video-discover.js +++ b/webui/static/video/video-discover.js @@ -543,7 +543,18 @@ } function renderShelves() { var host = $('[data-vdsc-shelves]'); if (!host) return; - host.innerHTML = buildSections().map(function (sec) { + var sections = buildSections(); + // A sticky "jump to section" bar (Netflix/Disney pattern) — the page is a deep + // stack of rails, so quick navigation makes it far easier to browse. Chips for + // async-only groups (foryou/collection/taste) start hidden, revealed when filled; + // chips for groups that prune away hide in lockstep (see reveal/pruneGroup). + var nav = ''; + host.innerHTML = nav + sections.map(function (sec) { var rails = sec.rails.map(lazyShelfHtml).join(''); // Groups with no static rails (async-only, e.g. gaps) start hidden — revealed when filled. var emptyCls = sec.rails.length ? '' : ' vdsc-group--empty'; @@ -552,16 +563,35 @@ '
' + rails + '
' + ''; }).join(''); + wireJumpNav(host); observeShelves(); } - // Reveal a group's header once an async loader has put rails in it. + // Smooth-scroll to a group when its jump chip is clicked (delegated, wired once). + function wireJumpNav(host) { + var nav = $('[data-vdsc-jumpnav]', host); + if (!nav || nav.getAttribute('data-wired')) return; + nav.setAttribute('data-wired', '1'); + nav.addEventListener('click', function (e) { + var btn = e.target.closest('[data-jump]'); if (!btn) return; + var g = $('[data-group="' + btn.getAttribute('data-jump') + '"]'); + if (g) { try { g.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (x) { g.scrollIntoView(); } } + }); + } + // Reveal a group's header once an async loader has put rails in it (+ its jump chip). function revealGroup(id) { var g = $('[data-group="' + id + '"]'); if (g) g.classList.remove('vdsc-group--empty'); + var chip = $('[data-jump="' + id + '"]'); + if (chip) chip.classList.remove('hidden'); } - // Hide a group whose body has no shelves left (every rail failed / returned nothing). + // Hide a group whose body has no shelves left (every rail failed / returned nothing) + its chip. function pruneGroup(g) { - if (g && !g.querySelector('.vdsc-shelf')) g.classList.add('vdsc-group--empty'); + if (g && !g.querySelector('.vdsc-shelf')) { + g.classList.add('vdsc-group--empty'); + var id = g.getAttribute('data-group'); + var chip = id && $('[data-jump="' + id + '"]'); + if (chip) chip.classList.add('hidden'); + } } function observeShelves() { if (state.io) state.io.disconnect(); diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 69f107c8..9b4cd94e 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -2303,6 +2303,26 @@ body[data-side="video"] #soulsync-toggle { display: none; } A group with no rails (async-only, not yet filled, or all rails dropped) is hidden entirely. */ .vdsc-group { margin: 0; } .vdsc-group--empty { display: none; } + +/* ── sticky 'jump to section' bar — quick nav across the deep rail stack ──────── */ +.vdsc-jumpnav { + position: sticky; top: 0; z-index: 6; + display: flex; gap: 8px; flex-wrap: nowrap; overflow-x: auto; + margin: 0 0 6px; padding: 11px 2px; + background: linear-gradient(180deg, rgba(10, 11, 16, 0.94) 35%, rgba(10, 11, 16, 0.55) 80%, transparent); + -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); + scrollbar-width: none; +} +.vdsc-jumpnav::-webkit-scrollbar { display: none; } +.vdsc-jump { + flex: 0 0 auto; padding: 7px 15px; border-radius: 999px; cursor: pointer; + border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.78); font-size: 12.5px; font-weight: 700; white-space: nowrap; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} +.vdsc-jump:hover { background: rgba(var(--accent-rgb, 88 101 242), 0.85); border-color: transparent; color: #fff; } +.vdsc-jump.hidden { display: none; } + .vdsc-group-head { font-size: 12.5px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase; color: rgba(255, 255, 255, 0.5); margin: 30px 0 16px; padding-bottom: 9px; border-bottom: 1px solid rgba(255, 255, 255, 0.07); }