From 39558d82b04c7da4919466a14fe1457eec6bf3ed Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 28 May 2026 00:25:23 +0100 Subject: [PATCH] fix(apps): drop apps-section cap when visible cards already fill the row at full width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous cap shrank the box to "exactly N cards at min width", which made 3 cards sit a few pixels short of the layout edge on a 3-column viewport while 4 cards (which wraps internally) ran edge-to-edge — visually inconsistent and the user flagged the gap. updateAppsCount now measures the parent's available inner width (minus the section's own 90px overhead: 22 margin + 22 padding + 1 border, doubled) and computes the natural column count the auto-fill grid would pick at full width. If visible cards >= that count, the function passes a sentinel (99) as --app-count so the formula overshoots the 100%-44px parent cap and yields the layout-edge box. Otherwise the cap still kicks in to hide card-shaped holes for 1-2 cards. Also wired a window resize listener in the constructor so dragging the window, snapping it, or opening devtools re-evaluates the decision — the natural column count is viewport-dependent. Signed-off-by: librelad --- .../js/components/app/apps-manager.js | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/containers/libreportal/frontend/js/components/app/apps-manager.js b/containers/libreportal/frontend/js/components/app/apps-manager.js index 0383667..61217e6 100755 --- a/containers/libreportal/frontend/js/components/app/apps-manager.js +++ b/containers/libreportal/frontend/js/components/app/apps-manager.js @@ -39,6 +39,11 @@ 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() { @@ -547,9 +552,11 @@ class AppsManager { }); } - // Sync --app-count on .apps-section to the number of currently-visible - // cards so the CSS max-width cap shrinks the glass box to exactly the - // cards it holds. Driven from render and from the sidebar search filter. + // Sync --app-count on .apps-section so the CSS max-width formula either + // shrinks the glass box around the visible cards (avoiding a card-shaped + // hole on the right) or — when the visible cards would already fill the + // row at full width — disengages the cap so the box runs edge-to-edge. + // Driven from render, sidebar search filter, and window resize. updateAppsCount() { const container = document.getElementById('apps-section'); if (!container) return; @@ -557,7 +564,27 @@ class AppsManager { container.querySelectorAll('.app-card').forEach(card => { if (card.style.display !== 'none') visible++; }); - container.style.setProperty('--app-count', Math.max(visible, 1)); + visible = Math.max(visible, 1); + + // How many columns the grid would naturally lay out at full width. + // If the visible count already meets that, suppress the cap (huge + // sentinel) so the formula gives way to the 100%-44px parent cap and + // the box reaches the layout edge. Without this the "exactly fills + // the row" case (e.g. 3 cards on a 3-col viewport) sits a few pixels + // shy of the edge while N+1 cards jumps straight to full width — + // visually inconsistent. + 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.