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() {