diff --git a/frontend/app.js b/frontend/app.js index ac91b06..9d96a90 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -1995,8 +1995,10 @@ function renderHomePlaylists() { wrap.classList.toggle('hidden', !lists.length); hero.classList.toggle('hidden', !!lists.length); // The hero is centered art; the grid wants the full box, so the placeholder - // switches from centering to stretching (and tightens its padding). + // switches from centering to stretching (and tightens its padding), and the + // stage drops its 16:9 lock so the grid gets the pane's real height. if (els.placeholder) els.placeholder.classList.toggle('showing-playlists', !!lists.length); + if (els.playerPane) els.playerPane.classList.toggle('home-playlists', !!lists.length); if (!lists.length) { grid.innerHTML = ''; return; } grid.innerHTML = ''; @@ -2024,6 +2026,20 @@ function renderHomePlaylists() { }); grid.appendChild(card); } + // Measure after layout — scrollHeight is meaningless until the cards lay out. + requestAnimationFrame(updateHomeGridScrollState); +} + +// With enough playlists the grid still has to scroll. Flag when it genuinely +// overflows (and whether it's at the end) so the CSS fades the bottom edge +// instead of slicing the last row against a hard border. +function updateHomeGridScrollState() { + const grid = document.getElementById('homePlGrid'); + if (!grid) return; + const overflows = grid.scrollHeight > grid.clientHeight + 1; + grid.classList.toggle('is-scrollable', overflows); + grid.classList.toggle('at-end', !overflows || + grid.scrollTop + grid.clientHeight >= grid.scrollHeight - 2); } function currentList() { @@ -3353,6 +3369,8 @@ function wireUI() { // ---- Empty-home playlist grid ---- $('homePlNewBtn').addEventListener('click', () => newPlaylist(null)); + $('homePlGrid').addEventListener('scroll', updateHomeGridScrollState, { passive: true }); + window.addEventListener('resize', updateHomeGridScrollState); els.searchForm.addEventListener('submit', async (e) => { e.preventDefault(); diff --git a/frontend/styles.css b/frontend/styles.css index 30356db..89cea7d 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -366,6 +366,21 @@ body::after { /* Empty home: the playlist grid takes the hero's place once playlists exist */ .player-placeholder.showing-playlists { padding: 16px; align-items: stretch; } +/* The 16:9 stage exists to hold a video, and on the empty home there isn't one + (controls / now-meta / up-next are hidden too). Constraining the playlist + grid to that short box clipped it mid-card, so let the stage take the pane's + height instead. Reverts the moment something plays — `.empty` comes off. */ +.player-pane.empty.home-playlists .player-stage { + aspect-ratio: auto; + flex: 1 1 auto; + min-height: 260px; +} +/* Bottom fade, applied only while the grid actually overflows and isn't + scrolled to the end — a half-row under a hard edge reads as broken. */ +.home-pl-grid.is-scrollable:not(.at-end) { + -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 30px), transparent 100%); + mask-image: linear-gradient(180deg, #000 calc(100% - 30px), transparent 100%); +} .home-pl { flex: 1 1 auto; align-self: stretch; min-width: 0; max-height: 100%; display: flex; flex-direction: column; gap: 10px; @@ -1425,6 +1440,9 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); } /* Empty-home playlist grid: the player stage is short on phones, so pack in more, smaller cards rather than one oversized full-width column. */ .player-placeholder.showing-playlists { padding: 12px; } + /* …but don't let it eat the whole screen — the list pane must stay reachable + without scrolling past a full-height board. */ + .player-pane.empty.home-playlists .player-stage { max-height: 58vh; } .home-pl-grid { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 10px; } .home-pl-title { font-size: 15px; } .home-pl-name { font-size: 12px; }