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); }