feat(briefwechsel): bump row typography and drop relative-year chip
The 168px-tall thumbnail tile was dominating rows where the text column only rendered at text-xs / text-sm — visually the right column sat half-empty. Three changes: - Title: text-sm → text-lg - Summary: text-sm → text-base - Meta + tag chips: text-xs → text-sm And remove the "vor N Jahren" chip entirely. The documentDate in the meta row already carries the temporal context and the chip was adding visual noise without new information. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -12,12 +12,12 @@ const hiddenTagCount = $derived(Math.max(0, tags.length - max));
|
|||||||
{#each displayedTags as tag (tag.id)}
|
{#each displayedTags as tag (tag.id)}
|
||||||
<span
|
<span
|
||||||
data-testid="thumb-row-tag"
|
data-testid="thumb-row-tag"
|
||||||
class="max-w-[140px] truncate rounded-full border border-line bg-surface px-2 py-0.5 text-xs text-ink-2"
|
class="max-w-[140px] truncate rounded-full border border-line bg-surface px-2 py-0.5 text-sm text-ink-2"
|
||||||
>{tag.name}</span
|
>{tag.name}</span
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
{#if hiddenTagCount > 0}
|
{#if hiddenTagCount > 0}
|
||||||
<span class="text-xs font-bold text-ink-3">+{hiddenTagCount}</span>
|
<span class="text-sm font-bold text-ink-3">+{hiddenTagCount}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import ConversationThumbnail from '$lib/components/ConversationThumbnail.svelte';
|
import ConversationThumbnail from '$lib/components/ConversationThumbnail.svelte';
|
||||||
import TagChipList from '$lib/components/TagChipList.svelte';
|
import TagChipList from '$lib/components/TagChipList.svelte';
|
||||||
import { formatDate } from '$lib/utils/date';
|
import { formatDate } from '$lib/utils/date';
|
||||||
import { relativeYearsDe } from '$lib/relativeTime';
|
|
||||||
import * as m from '$lib/paraglide/messages.js';
|
import * as m from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
type Person = { id: string; firstName?: string | null; lastName: string; displayName: string };
|
type Person = { id: string; firstName?: string | null; lastName: string; displayName: string };
|
||||||
@@ -28,13 +27,11 @@ type Doc = {
|
|||||||
let {
|
let {
|
||||||
doc,
|
doc,
|
||||||
isOut,
|
isOut,
|
||||||
showOtherParty,
|
showOtherParty
|
||||||
now = new Date()
|
|
||||||
}: {
|
}: {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
isOut: boolean;
|
isOut: boolean;
|
||||||
showOtherParty: boolean;
|
showOtherParty: boolean;
|
||||||
now?: Date;
|
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const title = $derived(doc.title || doc.originalFilename);
|
const title = $derived(doc.title || doc.originalFilename);
|
||||||
@@ -45,9 +42,6 @@ const otherPartyName = $derived(
|
|||||||
: (doc.sender?.displayName ?? '')
|
: (doc.sender?.displayName ?? '')
|
||||||
: ''
|
: ''
|
||||||
);
|
);
|
||||||
const relativeYearLabel = $derived(
|
|
||||||
doc.documentDate ? relativeYearsDe(new Date(doc.documentDate + 'T12:00:00'), now) : ''
|
|
||||||
);
|
|
||||||
const directionLabel = $derived(isOut ? m.row_direction_sent() : m.row_direction_received());
|
const directionLabel = $derived(isOut ? m.row_direction_sent() : m.row_direction_received());
|
||||||
const ariaLabel = $derived(
|
const ariaLabel = $derived(
|
||||||
`${directionLabel}: ${title}${doc.documentDate ? `, ${formatDate(doc.documentDate)}` : ''}`
|
`${directionLabel}: ${title}${doc.documentDate ? `, ${formatDate(doc.documentDate)}` : ''}`
|
||||||
@@ -63,23 +57,18 @@ const ariaLabel = $derived(
|
|||||||
>
|
>
|
||||||
<ConversationThumbnail doc={doc} />
|
<ConversationThumbnail doc={doc} />
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||||
<div class="flex items-baseline justify-between gap-3">
|
<div class="min-w-0 flex-1 truncate text-lg font-bold text-ink">
|
||||||
<div class="min-w-0 flex-1 truncate text-sm font-bold text-ink">
|
{title}
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
{#if relativeYearLabel}
|
|
||||||
<div class="shrink-0 text-xs text-ink-3">{relativeYearLabel}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if doc.summary}
|
{#if doc.summary}
|
||||||
<div class="line-clamp-2 text-sm text-ink-2 italic">
|
<div class="line-clamp-2 text-base text-ink-2 italic">
|
||||||
“{doc.summary}”
|
“{doc.summary}”
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-x-[6px] gap-y-1 text-xs text-ink-3">
|
<div class="flex flex-wrap items-center gap-x-[6px] gap-y-1 text-sm text-ink-3">
|
||||||
<span>{doc.documentDate ? formatDate(doc.documentDate) : '—'}</span>
|
<span>{doc.documentDate ? formatDate(doc.documentDate) : '—'}</span>
|
||||||
{#if doc.location}
|
{#if doc.location}
|
||||||
<span class="text-line">·</span>
|
<span class="text-line">·</span>
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(document.body.textContent).toContain('Liebe Anna');
|
expect(document.body.textContent).toContain('Liebe Anna');
|
||||||
@@ -49,8 +48,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: { ...baseDoc, title: '' },
|
doc: { ...baseDoc, title: '' },
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(document.body.textContent).toContain('liebe_anna.pdf');
|
expect(document.body.textContent).toContain('liebe_anna.pdf');
|
||||||
@@ -60,8 +58,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: true,
|
showOtherParty: true
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Out-going from Hans, other party is first receiver (Anna Schmidt)
|
// Out-going from Hans, other party is first receiver (Anna Schmidt)
|
||||||
@@ -72,8 +69,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: false,
|
isOut: false,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Anna is the receiver; in a bilateral list we suppress party names.
|
// Anna is the receiver; in a bilateral list we suppress party names.
|
||||||
@@ -84,8 +80,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const chips = document.querySelectorAll('[data-testid="thumb-row-tag"]');
|
const chips = document.querySelectorAll('[data-testid="thumb-row-tag"]');
|
||||||
@@ -93,38 +88,38 @@ describe('ThumbnailRow', () => {
|
|||||||
expect(document.body.textContent).toMatch(/\+2/);
|
expect(document.body.textContent).toMatch(/\+2/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders relative-year label derived from documentDate', () => {
|
it('does not render a relative-year label', () => {
|
||||||
|
// Document date is historical; we deliberately omit the "vor N Jahren"
|
||||||
|
// chip so the row can give vertical space to the title + summary.
|
||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1950-06-01 → 2026-06-01 = 76 years
|
|
||||||
expect(document.body.textContent).toContain('vor 76 Jahren');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hides the relative-year label when documentDate is in the future', () => {
|
|
||||||
// relativeYearsDe returns "" for future/invalid dates; the row must not
|
|
||||||
// then render an empty chip or print "vor 0 Jahren".
|
|
||||||
render(ThumbnailRow, {
|
|
||||||
doc: { ...baseDoc, documentDate: '2030-01-01' },
|
|
||||||
isOut: true,
|
|
||||||
showOtherParty: false,
|
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(document.body.textContent).not.toMatch(/vor \d+ Jahr/);
|
expect(document.body.textContent).not.toMatch(/vor \d+ Jahr/);
|
||||||
expect(document.body.textContent).not.toMatch(/vor weniger/);
|
expect(document.body.textContent).not.toMatch(/vor weniger/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the title at text-lg so the row uses its full vertical space', () => {
|
||||||
|
render(ThumbnailRow, {
|
||||||
|
doc: baseDoc,
|
||||||
|
isOut: true,
|
||||||
|
showOtherParty: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleEl = [...document.querySelectorAll('div')].find(
|
||||||
|
(el) => el.textContent?.trim() === 'Liebe Anna'
|
||||||
|
) as HTMLElement | undefined;
|
||||||
|
expect(titleEl, 'title element not found').toBeDefined();
|
||||||
|
expect(titleEl!.className).toContain('text-lg');
|
||||||
|
});
|
||||||
|
|
||||||
it('sets border-l class based on isOut', () => {
|
it('sets border-l class based on isOut', () => {
|
||||||
const { unmount } = render(ThumbnailRow, {
|
const { unmount } = render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
let link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
||||||
@@ -135,8 +130,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: false,
|
isOut: false,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
||||||
expect(link.className).toContain('border-l-accent');
|
expect(link.className).toContain('border-l-accent');
|
||||||
@@ -146,8 +140,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
const link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
||||||
@@ -163,8 +156,7 @@ describe('ThumbnailRow', () => {
|
|||||||
render(ThumbnailRow, {
|
render(ThumbnailRow, {
|
||||||
doc: baseDoc,
|
doc: baseDoc,
|
||||||
isOut: false,
|
isOut: false,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
const link = document.querySelector('a[href="/documents/d1"]') as HTMLElement;
|
||||||
@@ -179,8 +171,7 @@ describe('ThumbnailRow', () => {
|
|||||||
summary: 'safe <img src=x onerror="alert(1)"> text'
|
summary: 'safe <img src=x onerror="alert(1)"> text'
|
||||||
},
|
},
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// No real img tag from the summary, the ConversationThumbnail img is fine.
|
// No real img tag from the summary, the ConversationThumbnail img is fine.
|
||||||
@@ -199,8 +190,7 @@ describe('ThumbnailRow', () => {
|
|||||||
thumbnailAspect: 'PORTRAIT'
|
thumbnailAspect: 'PORTRAIT'
|
||||||
},
|
},
|
||||||
isOut: true,
|
isOut: true,
|
||||||
showOtherParty: false,
|
showOtherParty: false
|
||||||
now: new Date('2026-06-01T00:00:00Z')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(document.body.textContent).toContain('Ohne Datum');
|
expect(document.body.textContent).toContain('Ohne Datum');
|
||||||
|
|||||||
Reference in New Issue
Block a user