Files
mule-image/frontend/src/components/preview/PreviewFilmstrip.tsx
dtoro 2679214cb9 refactor: rename loupe to preview, bind to E and Space, fix empty viewer
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>
2026-04-07 22:47:52 +02:00

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>
)
}