diff --git a/containers/libreportal/frontend/css/apps.css b/containers/libreportal/frontend/css/apps.css index 1b4bddb..6c94b50 100644 --- a/containers/libreportal/frontend/css/apps.css +++ b/containers/libreportal/frontend/css/apps.css @@ -6,23 +6,25 @@ --app-min: 300px; --app-gap: 20px; display: grid; - /* JS sets --app-cols to a balanced track count: as many as fit the - row at min-width, but reduced by 1 if the natural count would - orphan a single card on the last row (e.g. 4 cards on a 3-col - viewport becomes a 2x2 grid instead of 3+1). Falls back to - auto-fill while JS hasn't run yet. Cards stretch via 1fr so the - last row never leaves a card-shaped hole — accepting some width - variation across categories in exchange for no internal gaps. */ - grid-template-columns: repeat(var(--app-cols, auto-fill), minmax(var(--app-min), 1fr)); + /* 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)); 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 for 2+ visible cards (sentinel from - apps-manager.js); for a single card the formula caps the box to - one card's worth instead of stretching it across the full row. + /* 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( diff --git a/containers/libreportal/frontend/js/components/app/apps-manager.js b/containers/libreportal/frontend/js/components/app/apps-manager.js index 89f9f7b..dff30e8 100755 --- a/containers/libreportal/frontend/js/components/app/apps-manager.js +++ b/containers/libreportal/frontend/js/components/app/apps-manager.js @@ -552,20 +552,10 @@ class AppsManager { }); } - // Sync --app-cols + --app-count on .apps-section so the grid lays out - // without orphans on the last row. - // - // --app-cols picks a balanced column count: as many as the row fits - // naturally, but reduced by 1 when the natural choice would leave a - // single card alone on the last row (e.g. 4 cards on a 3-col viewport - // becomes 2x2 instead of 3+1). 5+2, 6+3, etc are accepted — only the - // worst case (last row has exactly 1 orphan) is rebalanced. - // - // --app-count drives the CSS max-width cap: 1 visible card shrinks - // the box to one card's worth (stretching one card to 1000px+ looks - // bad); 2+ cards pass a sentinel (99) so the cap defers to the - // 100%-44px parent and the box runs edge-to-edge. - // + // 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'); @@ -583,22 +573,11 @@ class AppsManager { // 22px margin + 22px padding + 1px border, doubled. const parent = container.parentElement; const inside = parent ? Math.max(0, parent.clientWidth - 90) : 0; - const maxCols = inside > 0 + const naturalCols = inside > 0 ? Math.max(1, Math.floor((inside + gap) / (minCol + gap))) : visible; - - let cols = Math.min(visible, maxCols); - // Avoid orphaning a single card on the last row — reduce columns - // by one so the layout becomes (cols-1)+(cols-1)+… with a fuller - // tail. Don't drop below 2; a 1-col stack looks worse than an - // orphan. Only handles the immediate "last row is 1" case; - // last-row-of-2 etc are accepted as good enough. - if (cols > 2 && visible > cols && visible % cols === 1) { - cols--; - } - - container.style.setProperty('--app-cols', cols); - container.style.setProperty('--app-count', visible === 1 ? 1 : 99); + const effective = visible >= naturalCols ? 99 : visible; + container.style.setProperty('--app-count', effective); } // Client-side substring filter wired to the sidebar search box.