refactor(briefwechsel): extract TagChipList from ThumbnailRow
Lifts the three-chip-plus-"+N" tag row out of ThumbnailRow into a standalone TagChipList component so the chip cap + overflow policy lives in one place and can be reused on other surfaces (document detail header is a candidate). ThumbnailRow drops from 110 to ~90 lines and no longer owns tag-slicing logic — it just asks for the list with max=3. Behavior is byte-identical: same data-testid, same max cap, same "+N" overflow indicator. All ThumbnailRow row-level tag tests continue to pass against the new composition. Refs #305 Fixes @felixbrandt suggestion 1 from PR review Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
23
frontend/src/lib/components/TagChipList.svelte
Normal file
23
frontend/src/lib/components/TagChipList.svelte
Normal 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}
|
||||||
34
frontend/src/lib/components/TagChipList.svelte.spec.ts
Normal file
34
frontend/src/lib/components/TagChipList.svelte.spec.ts
Normal 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(/\+/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ConversationThumbnail from '$lib/components/ConversationThumbnail.svelte';
|
import ConversationThumbnail from '$lib/components/ConversationThumbnail.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 { relativeYearsDe } from '$lib/relativeTime';
|
||||||
|
|
||||||
@@ -36,8 +37,6 @@ let {
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const title = $derived(doc.title || doc.originalFilename);
|
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(
|
const otherPartyName = $derived(
|
||||||
showOtherParty
|
showOtherParty
|
||||||
? isOut
|
? isOut
|
||||||
@@ -91,19 +90,6 @@ const ariaLabel = $derived(
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if displayedTags.length > 0}
|
<TagChipList tags={doc.tags ?? []} max={3} />
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user