fix: use square cells in timeline grid

PhotoThumbnail computed cell height as size * min(aspectRatio, 1.5),
so portrait photos overflowed their row. The TanStack Virtual row
estimate is a single fixed value (thumbnailSize + gap), so any cell
taller than that pushed into the row below — visible as overlapping
thumbnails whenever a portrait shared a row with landscapes.

Switching to square cells (Lightroom Library default) means every row
is exactly the estimated height. The image still fills via object-cover,
just cropped on the long axis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 22:15:19 +02:00
parent d2155d9dd2
commit 6e6672f225

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { Star, Check, X, RefreshCw } from 'lucide-react'
import { Star, Check, Trash2, RefreshCw } from 'lucide-react'
import clsx from 'clsx'
import { photos as photosApi } from '../../services/api'
import type { Photo } from '../../types/photo'
@@ -28,9 +28,11 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick, onDoubleClick
const baseUrl = photosApi.getThumbnailUrl(photo.id, 'medium')
const thumbnailUrl = retryCount > 0 ? `${baseUrl}?retry=${retryCount}` : baseUrl
// Calculate aspect ratio for proper sizing (default to 1:1 if dimensions unknown)
const aspectRatio = (photo.height && photo.width) ? photo.height / photo.width : 1
const displayHeight = size * Math.min(aspectRatio, 1.5) // Cap height at 1.5x width
// Square cells (Lightroom-style grid). Variable-aspect cells previously
// overflowed their row because TanStack Virtual estimates row height as a
// single fixed value — portraits in a landscape row would overlap the row
// below. With object-cover the image still fills the cell, just cropped.
const displayHeight = size
const clearRetryTimer = () => {
if (retryTimerRef.current !== null) {
@@ -169,8 +171,8 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick, onDoubleClick
{photo.is_picked && (
<Check className="h-4 w-4 text-pick" />
)}
{photo.is_rejected && (
<X className="h-4 w-4 text-reject" />
{photo.is_trashed && (
<Trash2 className="h-4 w-4 text-reject" />
)}
</div>