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, containerWidth: number,
containerHeight: number, containerHeight: number,
rowCount: number, rowCount: number,
minAspect: number,
): TileRow => { ): TileRow => {
const gap = TILE_GAP const gap = TILE_GAP
const availableHeight = containerHeight - (rowCount - 1) * gap const availableHeight = containerHeight - (rowCount - 1) * gap
const rowHeight = availableHeight / rowCount const rowHeight = availableHeight / rowCount
const tileWidth = (containerWidth - (columnCount - 1) * gap) / columnCount const tileWidth = (containerWidth - (columnCount - 1) * gap) / columnCount
const fillAspect = tileWidth / rowHeight const fillAspect = tileWidth / rowHeight
// Cap at 16:9 to avoid horizontally stretching video (review feedback) const aspectRatio = Math.max(minAspect, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect))
const aspectRatio = Math.max(TILE_ASPECT_PORTRAIT, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect))
const tileHeight = tileWidth / aspectRatio const tileHeight = tileWidth / aspectRatio
return {columnCount, tileWidth, tileHeight, aspectRatio} return {columnCount, tileWidth, tileHeight, aspectRatio}
} }
@ -151,6 +151,7 @@ const buildMixedCandidate = (
columnCount: number, columnCount: number,
containerWidth: number, containerWidth: number,
containerHeight: number, containerHeight: number,
minAspect: number,
): AdaptiveTileGrid | undefined => { ): AdaptiveTileGrid | undefined => {
const gap = TILE_GAP const gap = TILE_GAP
const fullRowCount = Math.floor(tileCount / columnCount) const fullRowCount = Math.floor(tileCount / columnCount)
@ -163,10 +164,9 @@ const buildMixedCandidate = (
const tileWidthLast = (containerWidth - (remainder - 1) * gap) / remainder const tileWidthLast = (containerWidth - (remainder - 1) * gap) / remainder
if (tileWidthLast <= 0) return undefined if (tileWidthLast <= 0) return undefined
// Find a common height that fits both row types and container
const maxPossibleHeight = Math.min( const maxPossibleHeight = Math.min(
tileWidthFull / TILE_ASPECT_PORTRAIT, tileWidthFull / minAspect,
tileWidthLast / TILE_ASPECT_PORTRAIT, tileWidthLast / minAspect,
(containerHeight - (totalRowCount - 1) * gap) / totalRowCount, (containerHeight - (totalRowCount - 1) * gap) / totalRowCount,
) )
@ -234,6 +234,11 @@ export const computeAdaptiveGrid = (
): AdaptiveTileGrid | undefined => { ): AdaptiveTileGrid | undefined => {
if (tileCount <= 0 || containerWidth <= 0 || containerHeight <= 0) return 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 best: AdaptiveTileGrid | undefined
let bestScore: LayoutScore | undefined let bestScore: LayoutScore | undefined
@ -246,7 +251,13 @@ export const computeAdaptiveGrid = (
if (remainder === 0) { if (remainder === 0) {
// Uniform layout — all rows identical // 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 totalHeight = totalRowCount * row.tileHeight + (totalRowCount - 1) * TILE_GAP
const totalWidth = columnCount * row.tileWidth + (columnCount - 1) * TILE_GAP const totalWidth = columnCount * row.tileWidth + (columnCount - 1) * TILE_GAP
const rows: TileRow[] = [] const rows: TileRow[] = []
@ -261,7 +272,13 @@ export const computeAdaptiveGrid = (
} }
} else { } else {
// Mixed layout — last row has fewer tiles, make them wider // 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 if (!candidate) continue

View file

@ -270,7 +270,7 @@
<div <div
bind:clientWidth={gridWidth} bind:clientWidth={gridWidth}
bind:clientHeight={gridHeight} 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} {#if tileGrid}
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
{#each tileGrid.rows as row, rowIndex} {#each tileGrid.rows as row, rowIndex}