diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte b/frontend/src/routes/search/InterpretationChipRow.svelte
index 62a26c81..2356a774 100644
--- a/frontend/src/routes/search/InterpretationChipRow.svelte
+++ b/frontend/src/routes/search/InterpretationChipRow.svelte
@@ -6,6 +6,7 @@ import type { components } from '$lib/generated/api';
import type { ChipType } from './chip-types.js';
type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
+type TagHint = components['schemas']['TagHint'];
let {
interpretation,
@@ -19,7 +20,8 @@ type Chip =
| { key: string; type: 'sender'; label: string }
| { key: string; type: 'directional'; from: string; to: string }
| { key: string; type: 'date'; label: string }
- | { key: string; type: 'keyword'; value: string; label: string };
+ | { key: string; type: 'keyword'; value: string; label: string }
+ | { key: string; type: 'theme'; tag: TagHint; label: string };
// Locally removed chips. The parent remounts this component (via {#key}) on every
// new NL search, so this set never needs an explicit reset.
@@ -36,9 +38,22 @@ function dateRangeLabel(from: string | undefined, to: string | undefined): strin
return fromYear ?? toYear ?? '';
}
+function tagColorStyle(color: string | undefined): string | undefined {
+ if (!color) return undefined;
+ return `background-color: var(--c-tag-${color}); border-left-color: var(--c-tag-${color})`;
+}
+
const chips = $derived.by(() => {
const list: Chip[] = [];
- const { resolvedPersons, dateFrom, dateTo, keywords, keywordsApplied } = interpretation;
+ const {
+ resolvedPersons,
+ dateFrom,
+ dateTo,
+ keywords,
+ keywordsApplied,
+ resolvedTags,
+ tagsApplied
+ } = interpretation;
if (resolvedPersons.length >= 2) {
list.push({
@@ -74,6 +89,17 @@ const chips = $derived.by(() => {
}
}
+ if (tagsApplied) {
+ for (const tag of resolvedTags) {
+ list.push({
+ key: 'theme:' + tag.id,
+ type: 'theme',
+ tag,
+ label: `${m.search_chip_theme_prefix()}: ${tag.name}`
+ });
+ }
+ }
+
return list.filter((chip) => !removed.has(chip.key));
});
@@ -83,7 +109,13 @@ const showKeywordsNotApplied = $derived(
function remove(chip: Chip) {
removed.add(chip.key);
- onRemoveChip(chip.type, chip.type === 'keyword' ? chip.value : undefined);
+ if (chip.type === 'keyword') {
+ onRemoveChip(chip.type, chip.value);
+ } else if (chip.type === 'theme') {
+ onRemoveChip(chip.type, chip.tag.name);
+ } else {
+ onRemoveChip(chip.type, undefined);
+ }
}
const nameSpan = 'sm:max-w-[12rem] max-w-[8rem] truncate';
@@ -113,6 +145,21 @@ const removeButton =
×
+ {:else if chip.type === 'theme'}
+
+ {m.search_chip_theme_prefix()}:
+ {chip.tag.name}
+
+
{:else}
{chip.label}
diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
index 59b82e0b..9cefc628 100644
--- a/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
+++ b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
@@ -6,6 +6,7 @@ import type { components } from '$lib/generated/api';
type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
type PersonHint = components['schemas']['PersonHint'];
+type TagHint = components['schemas']['TagHint'];
afterEach(() => cleanup());
@@ -132,4 +133,79 @@ describe('InterpretationChipRow', () => {
.element(page.getByRole('button', { name: new RegExp('Absender') }))
.toBeInTheDocument();
});
+
+ // ── theme chips ─────────────────────────────────────────────────────────────
+
+ const makeTag = (id: string, name: string, color?: string): TagHint => ({ id, name, color });
+
+ it('renders theme chips when tagsApplied is true', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedTags: [makeTag('t1', 'Hochzeit')],
+ tagsApplied: true
+ }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(1);
+ await expect.element(page.getByText(/Thema: Hochzeit/)).toBeInTheDocument();
+ });
+
+ it('renders no theme chips when tagsApplied is false', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedTags: [makeTag('t1', 'Hochzeit')],
+ tagsApplied: false
+ }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(0);
+ });
+
+ it('renders exactly N theme chips for N resolved tags', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedTags: [makeTag('t1', 'Krieg'), makeTag('t2', 'Hochzeit'), makeTag('t3', 'Familie')],
+ tagsApplied: true
+ }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(3);
+ });
+
+ it('calls onRemoveChip with "theme" and tag name when × is clicked', async () => {
+ const onRemoveChip = vi.fn();
+ render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedTags: [makeTag('t1', 'Hochzeit')],
+ tagsApplied: true
+ }),
+ onRemoveChip
+ });
+ await page.getByRole('button', { name: /Thema: Hochzeit/ }).click();
+ expect(onRemoveChip).toHaveBeenCalledWith('theme', 'Hochzeit');
+ });
+
+ it('applies inline color style for a tag with a color', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedTags: [makeTag('t1', 'Hochzeit', 'sage')],
+ tagsApplied: true
+ }),
+ onRemoveChip: vi.fn()
+ });
+ const chip = container.querySelector('[data-chip-type="theme"]') as HTMLElement;
+ expect(chip.style.backgroundColor).toBeTruthy();
+ });
+
+ it('omits color style for a tag with no color', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedTags: [makeTag('t1', 'Hochzeit')],
+ tagsApplied: true
+ }),
+ onRemoveChip: vi.fn()
+ });
+ const chip = container.querySelector('[data-chip-type="theme"]') as HTMLElement;
+ expect(chip.getAttribute('style')).toBeFalsy();
+ });
});