From c19d4be3fe46a2b63ccbe611ddd570eccf9d0a89 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 14 Jun 2026 15:08:51 +0200 Subject: [PATCH] feat(timeline): add the root-tag chip component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TagChip renders a letter's primary root tag as a small rounded pill โ€” a decorative aria-hidden colored square (var(--c-tag-{token}), neutral when the color is null) plus the escaped tag name, prefixed by the sr-only theme label so color is never the only cue. Truncation is set inline so a long name ellipsizes without forcing the card into horizontal scroll, and the full name stays reachable via the chip title. Timeline-local by design โ€” lib/timeline may not import lib/tag (eslint boundary). Refs #835 Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/timeline/TagChip.svelte | 44 ++++++++++++++++++ .../src/lib/timeline/TagChip.svelte.spec.ts | 46 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 frontend/src/lib/timeline/TagChip.svelte create mode 100644 frontend/src/lib/timeline/TagChip.svelte.spec.ts diff --git a/frontend/src/lib/timeline/TagChip.svelte b/frontend/src/lib/timeline/TagChip.svelte new file mode 100644 index 00000000..94546da0 --- /dev/null +++ b/frontend/src/lib/timeline/TagChip.svelte @@ -0,0 +1,44 @@ + + + + {m.timeline_tag_chip_label()}: + {#if color} + + {:else} + + {/if} + {name} + diff --git a/frontend/src/lib/timeline/TagChip.svelte.spec.ts b/frontend/src/lib/timeline/TagChip.svelte.spec.ts new file mode 100644 index 00000000..4d31199b --- /dev/null +++ b/frontend/src/lib/timeline/TagChip.svelte.spec.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import * as m from '$lib/paraglide/messages.js'; +import TagChip from './TagChip.svelte'; + +afterEach(() => cleanup()); + +describe('TagChip', () => { + it('renders the tag name (REQ-008)', () => { + render(TagChip, { name: 'Familie', color: 'sage' }); + expect(document.body.textContent).toContain('Familie'); + }); + + it('prefixes the name with an sr-only theme label and a decorative square (REQ-011)', () => { + render(TagChip, { name: 'Krieg', color: 'sienna' }); + const srOnly = document.querySelector('.sr-only'); + expect(srOnly?.textContent).toContain(m.timeline_tag_chip_label()); + const square = document.querySelector('[data-testid="tag-chip-square"]'); + expect(square?.getAttribute('aria-hidden')).toBe('true'); + }); + + it('applies the color via var(--c-tag-{token}), never raw hex (REQ-009)', () => { + render(TagChip, { name: 'Krieg', color: 'sienna' }); + const square = document.querySelector('[data-testid="tag-chip-square"]') as HTMLElement; + expect(square.getAttribute('style')).toContain('var(--c-tag-sienna)'); + }); + + it('renders a neutral chip with no --c-tag- binding when color is null (REQ-007)', () => { + render(TagChip, { name: 'Allgemein', color: null }); + expect(document.body.textContent).toContain('Allgemein'); + expect(document.body.innerHTML).not.toContain('var(--c-tag-'); + }); + + it('exposes the full name as the chip title so a truncated name stays reachable (REQ-008a)', () => { + render(TagChip, { name: 'Briefe von der Front', color: 'sienna' }); + const chip = document.querySelector('[data-testid="tag-chip"]') as HTMLElement; + expect(chip.getAttribute('title')).toBe('Briefe von der Front'); + }); + + it('renders an HTML-bearing name as inert text, never markup (REQ-010)', () => { + const evil = ''; + render(TagChip, { name: evil, color: null }); + expect(document.body.textContent).toContain(evil); + expect(document.querySelector('img')).toBeNull(); + }); +});