fix: use 1:1 min aspect on landscape, scrollbar-gutter to prevent flicker

- minAspect=1:1 (square) on landscape containers, 3:4 on portrait
- scrollbar-gutter:stable on grid prevents scrollbar feedback loop
This commit is contained in:
mplorentz 2026-08-12 13:06:12 -04:00
parent 1df2b8f7b8
commit f099e97981
2 changed files with 25 additions and 8 deletions

View file

@ -122,14 +122,14 @@ const buildUniformRow = (
containerWidth: number,
containerHeight: number,
rowCount: number,
minAspect: number,
): TileRow => {
const gap = TILE_GAP
const availableHeight = containerHeight - (rowCount - 1) * gap
const rowHeight = availableHeight / rowCount
const tileWidth = (containerWidth - (columnCount - 1) * gap) / columnCount
const fillAspect = tileWidth / rowHeight
// Cap at 16:9 to avoid horizontally stretching video (review feedback)
const aspectRatio = Math.max(TILE_ASPECT_PORTRAIT, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect))
const aspectRatio = Math.max(minAspect, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect))
const tileHeight = tileWidth / aspectRatio
return {columnCount, tileWidth, tileHeight, aspectRatio}
}
@ -151,6 +151,7 @@ const buildMixedCandidate = (
columnCount: number,
containerWidth: number,
containerHeight: number,
minAspect: number,
): AdaptiveTileGrid | undefined => {
const gap = TILE_GAP
const fullRowCount = Math.floor(tileCount / columnCount)
@ -163,10 +164,9 @@ const buildMixedCandidate = (
const tileWidthLast = (containerWidth - (remainder - 1) * gap) / remainder
if (tileWidthLast <= 0) return undefined
// Find a common height that fits both row types and container
const maxPossibleHeight = Math.min(
tileWidthFull / TILE_ASPECT_PORTRAIT,
tileWidthLast / TILE_ASPECT_PORTRAIT,
tileWidthFull / minAspect,
tileWidthLast / minAspect,
(containerHeight - (totalRowCount - 1) * gap) / totalRowCount,
)
@ -234,6 +234,11 @@ export const computeAdaptiveGrid = (
): AdaptiveTileGrid | undefined => {
if (tileCount <= 0 || containerWidth <= 0 || containerHeight <= 0) return undefined
// On landscape viewports, bound tiles at 1:1 (square) so they don't
// become tall and narrow. On portrait viewports, allow 3:4 so more
// tiles fit on a phone screen.
const minAspect = containerWidth > containerHeight ? 1 : TILE_ASPECT_PORTRAIT
let best: AdaptiveTileGrid | undefined
let bestScore: LayoutScore | undefined
@ -246,7 +251,13 @@ export const computeAdaptiveGrid = (
if (remainder === 0) {
// Uniform layout — all rows identical
const row = buildUniformRow(columnCount, containerWidth, containerHeight, totalRowCount)
const row = buildUniformRow(
columnCount,
containerWidth,
containerHeight,
totalRowCount,
minAspect,
)
const totalHeight = totalRowCount * row.tileHeight + (totalRowCount - 1) * TILE_GAP
const totalWidth = columnCount * row.tileWidth + (columnCount - 1) * TILE_GAP
const rows: TileRow[] = []
@ -261,7 +272,13 @@ export const computeAdaptiveGrid = (
}
} else {
// Mixed layout — last row has fewer tiles, make them wider
candidate = buildMixedCandidate(tileCount, columnCount, containerWidth, containerHeight)
candidate = buildMixedCandidate(
tileCount,
columnCount,
containerWidth,
containerHeight,
minAspect,
)
}
if (!candidate) continue

View file

@ -270,7 +270,7 @@
<div
bind:clientWidth={gridWidth}
bind:clientHeight={gridHeight}
class="min-h-0 flex-1 overflow-y-auto">
class="min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]">
{#if tileGrid}
<div class="flex flex-col items-center gap-2">
{#each tileGrid.rows as row, rowIndex}