feat(briefwechsel): add ConversationThumbnail with aspect + page badge

Reads thumbnailAspect from the backend and swaps between a 120×168
portrait tile and a 168×120 landscape tile so postcards and photos
don't get cropped into a portrait frame. Shows a page-count badge
top-right for multi-page PDFs, and a pulsing skeleton while the
async thumbnail job hasn't run yet. URL assembly goes through the
existing thumbnailUrl helper so cache-busting stays consistent
with DocumentThumbnail.

Refs #305

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-23 14:40:15 +02:00
committed by marcel
parent a52d481a8e
commit 407bfbd5f1
2 changed files with 158 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import { thumbnailUrl } from '$lib/thumbnails';
type Doc = {
id: string;
thumbnailKey?: string;
thumbnailGeneratedAt?: string;
thumbnailAspect?: 'PORTRAIT' | 'LANDSCAPE';
pageCount?: number;
};
let { doc }: { doc: Doc } = $props();
const url = $derived(thumbnailUrl(doc));
const aspect = $derived(doc.thumbnailAspect ?? 'PORTRAIT');
const pageCount = $derived(doc.pageCount ?? 1);
const tileClass = $derived(aspect === 'LANDSCAPE' ? 'h-[120px] w-[168px]' : 'h-[168px] w-[120px]');
</script>
<div
data-testid="conv-thumb-tile"
data-aspect={aspect}
class="relative {tileClass} flex-shrink-0 overflow-hidden rounded-sm border border-line bg-white"
>
{#if url}
<img
src={url}
alt=""
class="h-full w-full object-cover object-top dark:mix-blend-multiply"
loading="lazy"
decoding="async"
/>
{:else}
<div
data-testid="conv-thumb-skeleton"
class="h-full w-full bg-line/60 motion-safe:animate-pulse"
aria-hidden="true"
></div>
{/if}
{#if pageCount > 1}
<span
data-testid="conv-thumb-page-badge"
class="absolute top-1 right-1 rounded-full bg-primary/90 px-1.5 py-0.5 text-xs leading-none font-bold text-surface"
>{pageCount}</span
>
{/if}
</div>