From 0ba8e980eab5913e65ceb214bf77a0622c9d081a Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 27 May 2026 20:54:39 +0100 Subject: [PATCH] ui(apps): shrink apps-section to visible-card count so few apps don't leave card-shaped gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The glass box was a CSS Grid with auto-fill columns of minmax(300px, 1fr), so it always painted across the full content area. With only 2 apps on a wide row the third/fourth column slots remained inside the border as empty space — visually a card-shaped hole. Drive the box's max-width off a --app-count CSS variable, capped at (100% - 44px) so it can't escape the layout's symmetric 22px gutter. margin: 22px auto keeps the horizontal padding symmetric in both the capped (auto-centers the smaller box) and full-width (auto collapses to 22+22) cases. --app-min (300/280 at the ≤1024 breakpoint) feeds both the grid template and the cap formula so the responsive column width stays a single source of truth. apps-manager.js sets --app-count to the count of visible .app-card elements after every render and after the sidebar search filter, so filtering down to 2 hits also collapses the box. Floor of 1 keeps the empty state usable. Mobile (≤768) overrides max-width to none — single column already fills, and the 10px gutter shouldn't be auto-centered. Signed-off-by: librelad --- containers/libreportal/frontend/css/apps.css | 21 ++++++++++++++++--- containers/libreportal/frontend/css/style.css | 2 +- .../js/components/app/apps-manager.js | 15 +++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/containers/libreportal/frontend/css/apps.css b/containers/libreportal/frontend/css/apps.css index ac8c164..4899b8f 100644 --- a/containers/libreportal/frontend/css/apps.css +++ b/containers/libreportal/frontend/css/apps.css @@ -3,14 +3,26 @@ /* App center cards, grid, tags, and detail view. Extracted from style.css. */ .apps-section { + --app-min: 300px; + --app-gap: 20px; display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; - margin: 22px; + grid-template-columns: repeat(auto-fill, minmax(var(--app-min), 1fr)); + gap: var(--app-gap); + margin: 22px auto; padding: 22px; background: rgba(var(--text-rgb), 0.025); border: 1px solid var(--border-subtle); border-radius: 16px; + /* Shrink the glass box to exactly the visible-card count so a row with + two apps doesn't leave a card-shaped hole on the right. --app-count + is set from apps-manager.js (render + search filter); the 100%-44px + cap keeps the same 22px symmetric gap from the layout edges when + there are enough cards to fill the row. Default 99 = no cap until + JS reports a real count. */ + max-width: min( + calc(100% - 44px), + calc(var(--app-count, 99) * var(--app-min) + (var(--app-count, 99) - 1) * var(--app-gap) + 44px) + ); } /* Override grid styling when showing loading content */ @@ -392,6 +404,9 @@ margin: 10px; padding: 12px; gap: 12px; + /* Mobile is always a single column — let it fill width regardless of + how many cards there are (no auto-centering, no card-count cap). */ + max-width: none; } /* Install screen action buttons stack full-width below the console. */ diff --git a/containers/libreportal/frontend/css/style.css b/containers/libreportal/frontend/css/style.css index 8251dbb..df42775 100755 --- a/containers/libreportal/frontend/css/style.css +++ b/containers/libreportal/frontend/css/style.css @@ -1352,7 +1352,7 @@ html[data-theme="nebula"]::after { /* Responsive Design */ @media (max-width: 1024px) { .apps-section { - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + --app-min: 280px; } } diff --git a/containers/libreportal/frontend/js/components/app/apps-manager.js b/containers/libreportal/frontend/js/components/app/apps-manager.js index 9493598..0383667 100755 --- a/containers/libreportal/frontend/js/components/app/apps-manager.js +++ b/containers/libreportal/frontend/js/components/app/apps-manager.js @@ -538,6 +538,7 @@ 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); @@ -546,6 +547,19 @@ 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. + 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++; + }); + container.style.setProperty('--app-count', Math.max(visible, 1)); + } + // 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. @@ -560,6 +574,7 @@ class AppsManager { const hay = card.dataset.search || ''; card.style.display = (!q || hay.includes(q)) ? '' : 'none'; }); + this.updateAppsCount(); } clearAppsSearch() {