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-gap: 20px;
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);
margin: 22px;
padding: 22px;
@ -15,14 +20,11 @@
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). Box is
left-aligned (margin: 22px, not auto) so cards stay where they were
before the cap was introduced the box just shortens on the right.
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
is set from apps-manager.js (render + search filter); the 100%-44px
cap keeps the same 22px gutter when the formula would otherwise
exceed parent width. Outer width under border-box (global default,
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.
Default 99 = no cap until JS reports a real count. */
buffer for sub-pixel rounding. Default 99 = no cap until JS reports. */
max-width: min(
calc(100% - 44px),
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
// 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.
// Sync --app-count on .apps-section so the CSS max-width formula shrinks
// the glass box around the visible cards (avoiding a card-shaped hole on
// the right). Cards themselves are fixed-width via the grid template, so
// card widths line up across categories — no "natural cols" measurement
// needed any more. Driven from render, sidebar search filter, and resize
// (the 100%-44px parent cap still depends on viewport width).
updateAppsCount() {
const container = document.getElementById('apps-section');
if (!container) return;
@ -564,27 +565,7 @@ class AppsManager {
container.querySelectorAll('.app-card').forEach(card => {
if (card.style.display !== 'none') visible++;
});
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);
container.style.setProperty('--app-count', Math.max(visible, 1));
}
// Client-side substring filter wired to the sidebar search box.