From f099e979810fcf78f6d95a0522e0684d00948825 Mon Sep 17 00:00:00 2001 From: mplorentz Date: Wed, 12 Aug 2026 13:06:12 -0400 Subject: [PATCH] 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 --- src/app/call.ts | 31 +++++++++++++++++----- src/app/components/VideoCallContent.svelte | 2 +- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/app/call.ts b/src/app/call.ts index 82bf0428..84ec1960 100644 --- a/src/app/call.ts +++ b/src/app/call.ts @@ -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 diff --git a/src/app/components/VideoCallContent.svelte b/src/app/components/VideoCallContent.svelte index 1692e415..42f57a40 100644 --- a/src/app/components/VideoCallContent.svelte +++ b/src/app/components/VideoCallContent.svelte @@ -270,7 +270,7 @@
+ class="min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]"> {#if tileGrid}
{#each tileGrid.rows as row, rowIndex}