diff --git a/frontend/.gitignore b/frontend/.gitignore
index 8617ce82..39081d9a 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -13,6 +13,9 @@ node_modules
.DS_Store
Thumbs.db
+# Leftover directory from branch work
+/src.main/
+
# Env
.env
.env.*
diff --git a/frontend/src/routes/geschichten/DocumentFilterChip.svelte b/frontend/src/routes/geschichten/DocumentFilterChip.svelte
new file mode 100644
index 00000000..4a0583b3
--- /dev/null
+++ b/frontend/src/routes/geschichten/DocumentFilterChip.svelte
@@ -0,0 +1,35 @@
+
+
+
+
+ {m.geschichten_filter_document_chip()}
+
+
+ {chipLabel}
+
+
+
diff --git a/frontend/src/routes/geschichten/DocumentFilterChip.svelte.spec.ts b/frontend/src/routes/geschichten/DocumentFilterChip.svelte.spec.ts
new file mode 100644
index 00000000..9b5b6576
--- /dev/null
+++ b/frontend/src/routes/geschichten/DocumentFilterChip.svelte.spec.ts
@@ -0,0 +1,87 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+
+vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
+
+import DocumentFilterChip from './DocumentFilterChip.svelte';
+
+afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+});
+
+const VALID_UUID = '11111111-2222-3333-4444-555555555555';
+
+describe('DocumentFilterChip', () => {
+ it('renders the resolved document title inside the chip', async () => {
+ render(DocumentFilterChip, {
+ props: {
+ id: VALID_UUID,
+ title: 'Brief an Oma',
+ onremove: vi.fn()
+ }
+ });
+
+ await expect.element(page.getByText(/Brief an Oma/)).toBeVisible();
+ });
+
+ it('renders the prefix label', async () => {
+ render(DocumentFilterChip, {
+ props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
+ });
+
+ await expect.element(page.getByText(/Gefiltert nach Brief/)).toBeVisible();
+ });
+
+ it('falls back to short UUID when title is null', async () => {
+ render(DocumentFilterChip, {
+ props: { id: VALID_UUID, title: null, onremove: vi.fn() }
+ });
+
+ await expect.element(page.getByText(/11111111/)).toBeVisible();
+ });
+
+ it('fires onremove when the remove button is clicked', async () => {
+ const onremove = vi.fn();
+ render(DocumentFilterChip, {
+ props: { id: VALID_UUID, title: 'Brief an Oma', onremove }
+ });
+
+ const btn = (await page
+ .getByRole('button', { name: /Brief an Oma aus Filter entfernen/ })
+ .element()) as HTMLElement;
+ btn.click();
+
+ await vi.waitFor(() => expect(onremove).toHaveBeenCalledOnce());
+ });
+
+ it('remove button aria-label references the resolved title', async () => {
+ render(DocumentFilterChip, {
+ props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
+ });
+
+ const btn = page.getByRole('button', { name: /Brief an Oma aus Filter entfernen/ });
+ await expect.element(btn).toBeVisible();
+ });
+
+ it('title= attribute equals the validated id, not a raw query string', async () => {
+ render(DocumentFilterChip, {
+ props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
+ });
+
+ const chip = document.querySelector('[title]');
+ expect(chip?.getAttribute('title')).toBe('Brief an Oma');
+ });
+
+ it('remove button has a minimum 44px touch target', async () => {
+ render(DocumentFilterChip, {
+ props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
+ });
+
+ const btn = (await page
+ .getByRole('button', { name: /Brief an Oma aus Filter entfernen/ })
+ .element()) as HTMLElement;
+ expect(btn.className).toMatch(/min-h-\[44px\]|min-h-11/);
+ });
+});