From bfda70079424c5d919e8e93c4ed54e40e4925dfd Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 28 May 2026 01:31:56 +0100 Subject: [PATCH] fix(apps): stretch cards to fill the row width so the box stays full-width on any zoom level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed-width tracks + cap formula kept the box pinned to "N cards at 328px" outer regardless of viewport size, so zooming out left a massive empty band between the box's right edge and the layout edge. The box was no longer "dynamic" in any real sense — it scaled with the card count, not with the available content. Switching grid-template-columns to repeat(auto-fit, minmax(--app-min, 1fr)) lets cards stretch to fill the row, and auto-fit collapses trailing empty tracks so a 2-card row in a 3-track-wide viewport doesn't leave a 328px hole at the end. Zoom in/out now just widens or narrows the cards; the box always reaches the layout edge. This drops the cross-category card-width uniformity that the earlier fixed-width pass introduced — a 2-card category now lays out as two wide cards while a 3-card category gets three narrower ones. That's mutually exclusive with "box always full width" without leaving holes, and the user has shifted priority to full-width-always. JS cleanup: dropped updateAppsCount + its window-resize listener + its callsites in renderApps/filterAppsByQuery — no more --app-count or column-count measurement needed when the grid handles everything natively. Signed-off-by: librelad --- containers/libreportal/frontend/css/apps.css | 38 ++++++------------- .../js/components/app/apps-manager.js | 35 ----------------- 2 files changed, 12 insertions(+), 61 deletions(-) diff --git a/containers/libreportal/frontend/css/apps.css b/containers/libreportal/frontend/css/apps.css index 8210032..c20b42a 100644 --- a/containers/libreportal/frontend/css/apps.css +++ b/containers/libreportal/frontend/css/apps.css @@ -3,41 +3,27 @@ /* App center cards, grid, tags, and detail view. Extracted from style.css. */ .apps-section { - /* Track min-width is the lever for "how many columns at this viewport". - auto-fill picks floor((content + gap) / (--app-min + gap)) tracks, so - bumping --app-min from 300 to 328 pushes typical 1280px-class laptops - from 3 cols → 2 cols where the third column would have been the - orphan that ran into a half-empty last row. Wider monitors still hit - 3+ cols (e.g. 1056px content fits 3 tracks of 328 with room to - spare), so density on bigger screens is unchanged. */ --app-min: 328px; --app-gap: 20px; display: grid; - /* Fixed-width tracks so cards stay exactly --app-min wide regardless - of how many are visible — a 2-card category lines up with a 3-card - category at the same X positions. auto-fill packs as many tracks - as the row fits at the current viewport. The partial-row orphan - (e.g. 4 cards on a 3-col viewport → 3+1 with the 4th alone on row - 2) is an accepted tradeoff: we tried rebalancing to 2x2, but that - dropped the cards-per-row count too aggressively for larger - viewports — keeping rows dense beats avoiding the orphan. */ - grid-template-columns: repeat(auto-fill, var(--app-min)); + /* auto-fit + minmax(min, 1fr): tracks stretch to fill the row width + so zooming out gives wider cards instead of a card-shaped hole on + the right (each track is at least --app-min wide; the 1fr upper + bound shares any remaining content width across them). auto-fit + collapses trailing empty tracks so a 2-card row in a 3-track-wide + viewport doesn't leave a fixed-width empty slot at the end. The + tradeoff is that card widths vary between categories (a 2-card + category gets fatter cards than a 3-card one) and between + viewport widths — that's the price of "box always reaches the + layout edge". The 22px margin handles the gutter from the parent; + no explicit max-width cap needed. */ + grid-template-columns: repeat(auto-fit, minmax(var(--app-min), 1fr)); gap: var(--app-gap); margin: 22px; padding: 22px; background: rgba(var(--text-rgb), 0.025); border: 1px solid var(--border-subtle); border-radius: 16px; - /* Box reaches the layout edge when the visible cards already fill a - natural row (sentinel from apps-manager.js sets --app-count to 99); - for fewer cards the formula caps the box to exactly that many so - no card-shaped hole sits to the right of a small category. - Outer under border-box (global, style.css:4): N*min + (N-1)*gap + - 44px padding + 2px border + 2px sub-pixel buffer. */ - max-width: min( - calc(100% - 44px), - calc(var(--app-count, 99) * var(--app-min) + (var(--app-count, 99) - 1) * var(--app-gap) + 48px) - ); } /* Override grid styling when showing loading content */ diff --git a/containers/libreportal/frontend/js/components/app/apps-manager.js b/containers/libreportal/frontend/js/components/app/apps-manager.js index dff30e8..9493598 100755 --- a/containers/libreportal/frontend/js/components/app/apps-manager.js +++ b/containers/libreportal/frontend/js/components/app/apps-manager.js @@ -39,11 +39,6 @@ class AppsManager { constructor() { this.cache = new Map(); this.setupTaskCompletionListener(); - // The dynamic-width box decides cap-vs-full based on the parent's - // current size, so re-run it when the window resizes (drags, snaps, - // devtools open). updateAppsCount is a fast no-op when no - // #apps-section is on screen. - window.addEventListener('resize', () => this.updateAppsCount()); } setupTaskCompletionListener() { @@ -543,7 +538,6 @@ class AppsManager { container.appendChild(card); }); this.populateInlineServiceButtons(); - this.updateAppsCount(); // Re-apply any active sidebar search so changing category // doesn't reveal apps that should be filtered out. if (this.appsSearchQuery) this.filterAppsByQuery(this.appsSearchQuery); @@ -552,34 +546,6 @@ class AppsManager { }); } - // Sync --app-count on .apps-section so the CSS max-width cap shrinks - // the box around a few visible cards (no card-shaped hole on the - // right for a half-empty row) but disengages when visible cards meet - // the natural full-width column count (box reaches the parent edge). - // Driven from render, sidebar search filter, and window resize. - updateAppsCount() { - const container = document.getElementById('apps-section'); - if (!container) return; - let visible = 0; - container.querySelectorAll('.app-card').forEach(card => { - if (card.style.display !== 'none') visible++; - }); - visible = Math.max(visible, 1); - - const style = getComputedStyle(container); - const minCol = parseFloat(style.getPropertyValue('--app-min')) || 300; - const gap = parseFloat(style.getPropertyValue('--app-gap')) || 20; - // Section eats 90px of parent's inner width before any card lands: - // 22px margin + 22px padding + 1px border, doubled. - const parent = container.parentElement; - const inside = parent ? Math.max(0, parent.clientWidth - 90) : 0; - const naturalCols = inside > 0 - ? Math.max(1, Math.floor((inside + gap) / (minCol + gap))) - : visible; - const effective = visible >= naturalCols ? 99 : visible; - container.style.setProperty('--app-count', effective); - } - // Client-side substring filter wired to the sidebar search box. // Cards carry data-search (built in createAppCard) so this stays // a single querySelectorAll + display toggle. @@ -594,7 +560,6 @@ class AppsManager { const hay = card.dataset.search || ''; card.style.display = (!q || hay.includes(q)) ? '' : 'none'; }); - this.updateAppsCount(); } clearAppsSearch() {