From 99528e6bea390cb77fda0e4ce44e1e04fdc60830 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 15 Jun 2026 10:34:29 +0200 Subject: [PATCH] feat(timeline): add the tinted Thema bucket-header chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fully-tinted root-tag chip for Thema-mode bucket headers (#827, REQ-015): fill and label both derive from the tag's --c-tag-* token via a color-mix wash so the label keeps ≥4.5:1 contrast in light and dark mode. A null or unknown token falls back to a neutral chip with no broken colour. Curator text is {...}-escaped (REQ-009). Distinct from the neutral per-letter TagChip. Refs #827 Co-Authored-By: Claude Opus 4.8 --- .../src/lib/timeline/BucketHeaderChip.svelte | 59 +++++++++++++++++++ .../timeline/BucketHeaderChip.svelte.spec.ts | 44 ++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 frontend/src/lib/timeline/BucketHeaderChip.svelte create mode 100644 frontend/src/lib/timeline/BucketHeaderChip.svelte.spec.ts diff --git a/frontend/src/lib/timeline/BucketHeaderChip.svelte b/frontend/src/lib/timeline/BucketHeaderChip.svelte new file mode 100644 index 00000000..9d12cab4 --- /dev/null +++ b/frontend/src/lib/timeline/BucketHeaderChip.svelte @@ -0,0 +1,59 @@ + + + + {m.timeline_tag_chip_label()}: + + {name} + diff --git a/frontend/src/lib/timeline/BucketHeaderChip.svelte.spec.ts b/frontend/src/lib/timeline/BucketHeaderChip.svelte.spec.ts new file mode 100644 index 00000000..61c926ad --- /dev/null +++ b/frontend/src/lib/timeline/BucketHeaderChip.svelte.spec.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import * as m from '$lib/paraglide/messages.js'; +import BucketHeaderChip from './BucketHeaderChip.svelte'; + +afterEach(() => cleanup()); + +describe('BucketHeaderChip (REQ-015/009)', () => { + it('renders the root-tag name', () => { + render(BucketHeaderChip, { name: 'Krieg', color: 'sienna' }); + expect(document.body.textContent).toContain('Krieg'); + }); + + it('tints the chip with var(--c-tag-{token}) for a known colour token (REQ-015)', () => { + render(BucketHeaderChip, { name: 'Krieg', color: 'sienna' }); + const chip = document.querySelector('[data-testid="bucket-header-chip"]') as HTMLElement; + expect(chip.getAttribute('style')).toContain('var(--c-tag-sienna)'); + }); + + it('renders a neutral chip with no --c-tag- binding when colour is null (REQ-015)', () => { + render(BucketHeaderChip, { name: 'Ohne Thema', color: null }); + expect(document.body.textContent).toContain('Ohne Thema'); + expect(document.body.innerHTML).not.toContain('var(--c-tag-'); + }); + + it('falls back to neutral for an unknown colour token, never a broken var (REQ-015)', () => { + // "krieg" is a §2 demo class name, not a real --c-tag-* token. + render(BucketHeaderChip, { name: 'Krieg', color: 'krieg' }); + expect(document.body.innerHTML).not.toContain('var(--c-tag-'); + }); + + it('prefixes the name with an sr-only theme label so colour is never the only cue', () => { + render(BucketHeaderChip, { name: 'Krieg', color: 'sienna' }); + const srOnly = document.querySelector('.sr-only'); + expect(srOnly?.textContent).toContain(m.timeline_tag_chip_label()); + }); + + it('renders an HTML-bearing name as inert text, never markup (REQ-009)', () => { + const evil = ''; + render(BucketHeaderChip, { name: evil, color: null }); + expect(document.body.textContent).toContain(evil); + expect(document.querySelector('img')).toBeNull(); + }); +});