The loupe view is now called "preview" everywhere — file paths, type
names, store actions, and the contextual hint pill. There's a single
preview action bound to E and Space (Enter is gone); double-click on a
thumbnail still works. Both shortcuts toggle: open from grid, close
from preview.
This commit also folds in the fix for the "preview shows nothing" bug
the user just hit:
- Extract usePhotosQuery into frontend/src/hooks/usePhotosQuery.ts so
Timeline, PreviewView, and App.tsx all share one query — and one
cache entry. Previously PreviewView and App.tsx looked the cache up
by ['photos'], but the Timeline query key gained the filter params
(['photos', filterParams]) when the filter bar shipped, so the
lookup returned undefined and the preview rendered "No photo to
display". App.tsx's getFirstPhotoId callback had the same bug.
- Harden PreviewImage: render the <img> immediately and overlay the
spinner with absolute positioning, instead of toggling opacity-0 →
opacity-100 on load. The previous opacity-toggle could leave the
image stuck invisible if the load event raced with a key change.
- Add { preventDefault: true } to every useHotkeys call so single
letter shortcuts (1-5, P, X, U) no longer leak into Firefox quick-
find, and Cmd/Ctrl+F no longer triggers the browser find toolbar.
Files renamed:
components/loupe/LoupeView.tsx -> components/preview/PreviewView.tsx
components/loupe/LoupeImage.tsx -> components/preview/PreviewImage.tsx
components/loupe/LoupeFilmstrip.tsx -> components/preview/PreviewFilmstrip.tsx
components/loupe/loupeSrc.ts -> components/preview/previewSrc.ts
Symbol renames: openLoupe→openPreview, closeLoupe→closePreview, the
viewMode 'loupe' tag → 'preview', and all the LoupeXxx component and
helper exports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import clsx from 'clsx'
|
|
import type { Photo } from '../../types/photo'
|
|
import { photos as photosApi } from '../../services/api'
|
|
|
|
interface PreviewFilmstripProps {
|
|
photos: Photo[]
|
|
currentIndex: number
|
|
onSelect: (id: string) => void
|
|
}
|
|
|
|
const CELL_SIZE = 72
|
|
|
|
export function PreviewFilmstrip({ photos, currentIndex, onSelect }: PreviewFilmstripProps) {
|
|
const activeRef = useRef<HTMLButtonElement>(null)
|
|
|
|
useEffect(() => {
|
|
activeRef.current?.scrollIntoView({
|
|
block: 'nearest',
|
|
inline: 'center',
|
|
behavior: 'smooth',
|
|
})
|
|
}, [currentIndex])
|
|
|
|
return (
|
|
<div className="flex h-24 shrink-0 items-center gap-1 overflow-x-auto border-t border-border bg-surface px-2 py-2">
|
|
{photos.map((photo, index) => {
|
|
const isActive = index === currentIndex
|
|
return (
|
|
<button
|
|
key={photo.id}
|
|
ref={isActive ? activeRef : null}
|
|
onClick={() => onSelect(photo.id)}
|
|
className={clsx(
|
|
'shrink-0 overflow-hidden rounded-sm transition-all',
|
|
'hover:opacity-100',
|
|
isActive
|
|
? 'ring-2 ring-primary opacity-100'
|
|
: 'opacity-60'
|
|
)}
|
|
style={{ width: CELL_SIZE, height: CELL_SIZE }}
|
|
title={photo.filename}
|
|
>
|
|
<img
|
|
src={photosApi.getThumbnailUrl(photo.id, 'small')}
|
|
alt={photo.filename}
|
|
loading="lazy"
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|