feat(briefwechsel): thumbnail rows with summary quote and bilateral distribution bar (#305) #311

Merged
marcel merged 30 commits from feat/issue-305-briefwechsel-thumbnail-rows into main 2026-04-23 21:38:57 +02:00
3 changed files with 59 additions and 16 deletions
Showing only changes of commit 3b3b551d84 - Show all commits

View File

@@ -0,0 +1,23 @@
<script lang="ts">
type Tag = { id: string; name: string };
let { tags, max }: { tags: Tag[]; max: number } = $props();
const displayedTags = $derived(tags.slice(0, max));
const hiddenTagCount = $derived(Math.max(0, tags.length - max));
</script>
{#if tags.length > 0}
<div class="flex flex-wrap items-center gap-1 pt-0.5">
{#each displayedTags as tag (tag.id)}
<span
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"
>{tag.name}</span
>
{/each}
{#if hiddenTagCount > 0}
<span class="text-xs font-bold text-ink-3">+{hiddenTagCount}</span>
{/if}
</div>
{/if}

View File

@@ -0,0 +1,34 @@
import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import TagChipList from './TagChipList.svelte';
afterEach(() => {
cleanup();
});
const makeTags = (n: number) =>
Array.from({ length: n }, (_, i) => ({ id: `t${i}`, name: `Tag${i}` }));
describe('TagChipList', () => {
it('renders all tags as chips when under the cap', () => {
render(TagChipList, { tags: makeTags(2), max: 3 });
const chips = document.querySelectorAll('[data-testid="thumb-row-tag"]');
expect(chips).toHaveLength(2);
expect(document.body.textContent).not.toMatch(/\+/);
});
it('caps visible chips at max and renders +N for the remainder', () => {
render(TagChipList, { tags: makeTags(5), max: 3 });
const chips = document.querySelectorAll('[data-testid="thumb-row-tag"]');
expect(chips).toHaveLength(3);
expect(document.body.textContent).toMatch(/\+2/);
});
it('renders nothing when tags is empty', () => {
render(TagChipList, { tags: [], max: 3 });
const chips = document.querySelectorAll('[data-testid="thumb-row-tag"]');
expect(chips).toHaveLength(0);
expect(document.body.textContent).not.toMatch(/\+/);
});
});

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import ConversationThumbnail from '$lib/components/ConversationThumbnail.svelte';
import TagChipList from '$lib/components/TagChipList.svelte';
import { formatDate } from '$lib/utils/date';
import { relativeYearsDe } from '$lib/relativeTime';
@@ -36,8 +37,6 @@ let {
} = $props();
const title = $derived(doc.title || doc.originalFilename);
const displayedTags = $derived((doc.tags ?? []).slice(0, 3));
const hiddenTagCount = $derived(Math.max(0, (doc.tags ?? []).length - 3));
const otherPartyName = $derived(
showOtherParty
? isOut
@@ -91,19 +90,6 @@ const ariaLabel = $derived(
{/if}
</div>
{#if displayedTags.length > 0}
<div class="flex flex-wrap items-center gap-1 pt-0.5">
{#each displayedTags as tag (tag.id)}
<span
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"
>{tag.name}</span
>
{/each}
{#if hiddenTagCount > 0}
<span class="text-xs font-bold text-ink-3">+{hiddenTagCount}</span>
{/if}
</div>
{/if}
<TagChipList tags={doc.tags ?? []} max={3} />
</div>
</a>