style(doclist): improve gallery view tile visuals and image preview readiness

Update gallery view document tiles with refined border, shadow, and background
styles for better visual feedback and accessibility. Enhance image preview
component to immediately reveal cached images by checking decode state on mount,
preventing opacity glitches on remount. Adjust gallery rail scrolling to reset
on document list changes for consistent navigation.
This commit is contained in:
Richard R 2026-05-28 04:51:04 -06:00
parent 85aff2c1be
commit f569c8ded2
2 changed files with 52 additions and 10 deletions

View file

@ -62,6 +62,7 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
lowerName.endsWith('.mkd'));
const containerRef = useRef<HTMLDivElement | null>(null);
const imageRef = useRef<HTMLImageElement | null>(null);
const [isVisible, setIsVisible] = useState(false);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [isImageReady, setIsImageReady] = useState(false);
@ -206,6 +207,17 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
setIsImageReady(false);
}, [imagePreview]);
// Cached blob/http sources can already be decoded before React's onLoad handler
// runs on remount, leaving opacity at 0. Promote already-complete images.
useEffect(() => {
if (!imagePreview) return;
const img = imageRef.current;
if (!img) return;
if (img.complete && img.naturalWidth > 0) {
setIsImageReady(true);
}
}, [imagePreview]);
const gradientClass = isPDF
? 'from-red-500/80 via-red-400/60 to-red-600/80'
: isEPUB
@ -246,6 +258,7 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
) : null}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
ref={imageRef}
src={imagePreview}
alt={`${doc.name} preview`}
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-150 ${isImageReady ? 'opacity-100' : 'opacity-0'}`}

View file

@ -1,7 +1,7 @@
'use client';
import Link from 'next/link';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import type { DocumentListDocument } from '@/types/documents';
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
@ -65,12 +65,13 @@ function GalleryThumb({
ref={setRefs}
data-doc-tile
onClick={onClick}
aria-current={active ? 'true' : undefined}
className={
'shrink-0 cursor-pointer rounded-md overflow-hidden border transition-all duration-200 ease-out snap-start ' +
'group relative w-[98px] sm:w-[110px] shrink-0 cursor-pointer rounded-lg overflow-hidden border bg-base snap-start transition-all duration-200 ease-out ' +
(active
? 'border-accent ring-1 ring-accent w-[110px]'
: 'border-offbase hover:border-accent hover:scale-[1.01] w-[88px]') +
(isOver && canDrop ? ' ring-1 ring-accent' : '') +
? 'border-accent shadow-[0_10px_24px_-18px_rgba(0,0,0,0.85)] -translate-y-0.5'
: 'border-offbase hover:border-accent hover:-translate-y-0.5') +
(isOver && canDrop ? ' border-accent' : '') +
(isDragging ? ' opacity-50' : '')
}
title={doc.name}
@ -78,9 +79,27 @@ function GalleryThumb({
<div className="aspect-[3/4] bg-base">
<DocumentPreview doc={doc} />
</div>
<div className="px-1.5 py-1 flex items-center gap-1 bg-base">
<KindIcon doc={doc} className="w-3 h-3 shrink-0 text-muted" />
<span className="truncate text-[10px] text-foreground">{doc.name}</span>
<div
className={
'px-2 py-1.5 flex items-center gap-1.5 border-t transition-colors duration-200 ' +
(active ? 'bg-offbase border-accent' : 'bg-base border-offbase')
}
>
<KindIcon
doc={doc}
className={
'w-3 h-3 shrink-0 transition-colors duration-200 ' +
(active ? 'text-accent' : 'text-muted')
}
/>
<span
className={
'truncate text-[10px] leading-none transition-colors duration-200 ' +
(active ? 'text-accent font-medium' : 'text-foreground')
}
>
{doc.name}
</span>
</div>
</div>
);
@ -92,6 +111,7 @@ export function GalleryView({
onMergeIntoFolder,
}: GalleryViewProps) {
const { setVisibleOrder } = useDocumentSelection();
const railRef = useRef<HTMLDivElement | null>(null);
const [activeIdx, setActiveIdx] = useState(0);
const activeDoc = useMemo(() => documents[activeIdx], [documents, activeIdx]);
@ -105,6 +125,12 @@ export function GalleryView({
}
}, [documents.length, activeIdx]);
useEffect(() => {
const rail = railRef.current;
if (!rail) return;
rail.scrollLeft = 0;
}, [documents.length]);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
@ -156,8 +182,11 @@ export function GalleryView({
)}
</div>
<div className="shrink-0 border-t border-offbase bg-base">
<div className="flex gap-2 overflow-x-auto p-2 snap-x snap-mandatory">
<div className="shrink-0 border-t border-offbase bg-gradient-to-b from-base to-offbase/30">
<div
ref={railRef}
className="flex gap-2.5 overflow-x-auto pl-4 pr-3 pt-2.5 pb-1.5 snap-x snap-proximity scroll-pl-4 sm:scroll-pl-5 scroll-pr-3 sm:scroll-pr-4"
>
{documents.map((doc, i) => (
<GalleryThumb
key={`${doc.type}-${doc.id}`}