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:
parent
1df2b8f7b8
commit
f099e97981
2 changed files with 25 additions and 8 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue