fix(apps): fixed-width grid tracks so card widths line up across categories

The auto-fill minmax(300px, 1fr) template stretched cards to fill the
glass box, so a 2-card category landed at ~301px each (the box
shrunk-and-stretched to a hair over 2*300) while a 3-card category
(box now full-width) landed at ~323px each. Cards visibly didn't
align between categories — the user spotted the 22px difference.

Switching the grid template to fixed-width tracks
(repeat(auto-fill, var(--app-min))) means cards are always exactly
--app-min (300px / 280px at ≤1024) regardless of how many are
visible. Card positions and widths line up across every category.

The natural-columns sentinel from the previous pass is no longer
load-bearing — with fixed-width cards, "full width" at high N gives
no extra card-width benefit, only trailing space inside the box.
updateAppsCount drops the measurement step and just sets the visible
count, letting the formula shrink the box around the cards.

Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-28 00:33:41 +01:00
parent e6fbfb5f97
commit de6acc1f92
2 changed files with 17 additions and 34 deletions

View File

@ -6,7 +6,12 @@
--app-min: 300px; --app-min: 300px;
--app-gap: 20px; --app-gap: 20px;
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--app-min), 1fr)); /* Fixed-width tracks (not minmax/1fr) 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. 1fr stretching used
to rubber-band card widths between categories as the box width
changed. */
grid-template-columns: repeat(auto-fill, var(--app-min));
gap: var(--app-gap); gap: var(--app-gap);
margin: 22px; margin: 22px;
padding: 22px; padding: 22px;
@ -15,14 +20,11 @@
border-radius: 16px; border-radius: 16px;
/* Shrink the glass box to exactly the visible-card count so a row with /* 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 two apps doesn't leave a card-shaped hole on the right. --app-count
is set from apps-manager.js (render + search filter). Box is is set from apps-manager.js (render + search filter); the 100%-44px
left-aligned (margin: 22px, not auto) so cards stay where they were cap keeps the same 22px gutter when the formula would otherwise
before the cap was introduced the box just shortens on the right. exceed parent width. Outer width under border-box (global default,
The 100%-44px cap honours the same 22px gutter at full width. The
formula is the outer width under border-box (the global default from
style.css:4): N*min + (N-1)*gap + 44px padding + 2px border + 2px style.css:4): N*min + (N-1)*gap + 44px padding + 2px border + 2px
buffer for sub-pixel rounding so 2 cards reliably stay on one row. buffer for sub-pixel rounding. Default 99 = no cap until JS reports. */
Default 99 = no cap until JS reports a real count. */
max-width: min( max-width: min(
calc(100% - 44px), calc(100% - 44px),
calc(var(--app-count, 99) * var(--app-min) + (var(--app-count, 99) - 1) * var(--app-gap) + 48px) calc(var(--app-count, 99) * var(--app-min) + (var(--app-count, 99) - 1) * var(--app-gap) + 48px)

View File

@ -552,11 +552,12 @@ class AppsManager {
}); });
} }
// Sync --app-count on .apps-section so the CSS max-width formula either // Sync --app-count on .apps-section so the CSS max-width formula shrinks
// shrinks the glass box around the visible cards (avoiding a card-shaped // the glass box around the visible cards (avoiding a card-shaped hole on
// hole on the right) or — when the visible cards would already fill the // the right). Cards themselves are fixed-width via the grid template, so
// row at full width — disengages the cap so the box runs edge-to-edge. // card widths line up across categories — no "natural cols" measurement
// Driven from render, sidebar search filter, and window resize. // needed any more. Driven from render, sidebar search filter, and resize
// (the 100%-44px parent cap still depends on viewport width).
updateAppsCount() { updateAppsCount() {
const container = document.getElementById('apps-section'); const container = document.getElementById('apps-section');
if (!container) return; if (!container) return;
@ -564,27 +565,7 @@ class AppsManager {
container.querySelectorAll('.app-card').forEach(card => { container.querySelectorAll('.app-card').forEach(card => {
if (card.style.display !== 'none') visible++; if (card.style.display !== 'none') visible++;
}); });
visible = Math.max(visible, 1); container.style.setProperty('--app-count', 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. // Client-side substring filter wired to the sidebar search box.