fix: square timeline cells that fully fill each row
The grid was leaving horizontal space unused for two reasons:
1. Width measurement was based on parentRef.clientWidth - PADDING*2,
which is fragile to padding/box-sizing/scrollbar mismatches and was
off by enough pixels in practice to drop a column. Replace with a
1px-tall normal-flow sentinel rendered inside the inner virtualizer
wrapper at the exact horizontal extent rows render at. ResizeObserver
on the sentinel gives the authoritative row width — no padding
subtraction, no scrollbar guesswork.
2. The flex layout with explicit per-cell px widths accumulated floor()
rounding error and let cells drift away from square. Switch to a CSS
grid with fixed-px tracks (`repeat(cols, ${cellSize}px)` + matching
`gridAutoRows`) so every track is exactly cellSize wide AND tall. By
construction `cols × cellSize + (cols-1) × gap == measured width`,
so the row fills edge-to-edge with cells that are guaranteed square.
PhotoThumbnail gains an opt-in `fill` prop the timeline uses to switch
its inline width/height to 100%, so the cell stretches to whatever the
parent grid track gives it. Heap sidebar / non-grid callers still get
explicit `size`-by-`size` square thumbnails as before.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,13 @@ const AUTO_RETRY_DELAYS = [1500, 3500, 7000, 12000, 20000]
|
||||
interface PhotoThumbnailProps {
|
||||
photo: Photo
|
||||
size: number
|
||||
/** When true, the cell stretches to fill its parent (100% width +
|
||||
* 100% height) and ignores `size` for the box dimensions. Used by
|
||||
* the Timeline grid where the parent is a CSS grid track of 1fr —
|
||||
* this is what guarantees the row fills the container without any
|
||||
* rounding gap on the right. The heap sidebar leaves this off so
|
||||
* thumbnails stay at the explicit `size`. */
|
||||
fill?: boolean
|
||||
isSelected: boolean
|
||||
/** True when the photo belongs to the currently active heap. */
|
||||
isInActiveHeap?: boolean
|
||||
@@ -30,6 +37,7 @@ interface PhotoThumbnailProps {
|
||||
export function PhotoThumbnail({
|
||||
photo,
|
||||
size,
|
||||
fill = false,
|
||||
isSelected,
|
||||
isInActiveHeap = false,
|
||||
activeHeapName = null,
|
||||
@@ -51,6 +59,12 @@ export function PhotoThumbnail({
|
||||
// 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.
|
||||
//
|
||||
// The cell stretches to whatever width the parent grid track gives it
|
||||
// (via width:100% + aspect-ratio:1) so the timeline's CSS grid can hand
|
||||
// out 1fr columns and we never leave horizontal space unused. `size`
|
||||
// remains the *minimum* track width and the fallback when there's no
|
||||
// parent grid (e.g. heap thumbnails).
|
||||
const displayHeight = size
|
||||
|
||||
const clearRetryTimer = () => {
|
||||
@@ -131,10 +145,11 @@ export function PhotoThumbnail({
|
||||
'ring-2 ring-primary ring-offset-2 ring-offset-bg shadow-lg',
|
||||
!imageLoaded && 'bg-surface animate-pulse'
|
||||
)}
|
||||
style={{
|
||||
width: size,
|
||||
height: displayHeight,
|
||||
}}
|
||||
style={
|
||||
fill
|
||||
? { width: '100%', height: '100%' }
|
||||
: { width: size, height: displayHeight }
|
||||
}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
draggable
|
||||
|
||||
Reference in New Issue
Block a user