From 94b8117c17ac10ae9203f118d9e09ba82e1e82f6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 16 Jun 2026 19:02:44 +0200 Subject: [PATCH] =?UTF-8?q?refactor(shared):=20re-skin=20ThemeToggle=20ont?= =?UTF-8?q?o=20SegmentedControl=20(=C2=A76=20tokens)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds theme_segment_light/dark/label i18n keys (de/en/es). Refs #857 Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 3 + frontend/messages/en.json | 3 + frontend/messages/es.json | 3 + .../lib/shared/primitives/ThemeToggle.svelte | 67 +++++++------------ .../primitives/ThemeToggle.svelte.spec.ts | 55 ++++++++++----- 5 files changed, 71 insertions(+), 60 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 8a08c280..60916b8e 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -32,6 +32,9 @@ "layout_menu_close": "Menü schließen", "theme_toggle_to_light": "Zu hellem Design wechseln", "theme_toggle_to_dark": "Zu dunklem Design wechseln", + "theme_toggle_label": "Farbschema", + "theme_segment_light": "Hell", + "theme_segment_dark": "Dunkel", "btn_save": "Speichern", "btn_cancel": "Abbrechen", "btn_confirm": "Bestätigen", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 2e498c39..a6bf401d 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -32,6 +32,9 @@ "layout_menu_close": "Close menu", "theme_toggle_to_light": "Switch to light mode", "theme_toggle_to_dark": "Switch to dark mode", + "theme_toggle_label": "Color scheme", + "theme_segment_light": "Light", + "theme_segment_dark": "Dark", "btn_save": "Save", "btn_cancel": "Cancel", "btn_confirm": "Confirm", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index b4883eb2..080e25a0 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -32,6 +32,9 @@ "layout_menu_close": "Cerrar menú", "theme_toggle_to_light": "Cambiar a modo claro", "theme_toggle_to_dark": "Cambiar a modo oscuro", + "theme_toggle_label": "Esquema de color", + "theme_segment_light": "Claro", + "theme_segment_dark": "Oscuro", "btn_save": "Guardar", "btn_cancel": "Cancelar", "btn_confirm": "Confirmar", diff --git a/frontend/src/lib/shared/primitives/ThemeToggle.svelte b/frontend/src/lib/shared/primitives/ThemeToggle.svelte index 834f765c..df3264d3 100644 --- a/frontend/src/lib/shared/primitives/ThemeToggle.svelte +++ b/frontend/src/lib/shared/primitives/ThemeToggle.svelte @@ -1,6 +1,7 @@ - + +
+ +
diff --git a/frontend/src/lib/shared/primitives/ThemeToggle.svelte.spec.ts b/frontend/src/lib/shared/primitives/ThemeToggle.svelte.spec.ts index 546a22ab..f0ce5687 100644 --- a/frontend/src/lib/shared/primitives/ThemeToggle.svelte.spec.ts +++ b/frontend/src/lib/shared/primitives/ThemeToggle.svelte.spec.ts @@ -8,38 +8,59 @@ afterEach(() => { localStorage.removeItem('theme'); }); -describe('ThemeToggle — label derivation (light mode)', () => { +describe('ThemeToggle — renders segments (light mode)', () => { beforeEach(() => { localStorage.setItem('theme', 'light'); }); - it('aria-label invites switching to dark mode when theme is light', async () => { + it('renders a radiogroup with Hell and Dunkel segments', async () => { render(ThemeToggle); - const btn = await page.getByRole('button').element(); - expect(btn.getAttribute('aria-label')).toBe('Zu dunklem Design wechseln'); + expect(document.querySelector('[role="radiogroup"]')).not.toBeNull(); + await expect.element(page.getByRole('radio', { name: 'Hell' })).toBeVisible(); + await expect.element(page.getByRole('radio', { name: 'Dunkel' })).toBeVisible(); }); - it('title equals aria-label in light mode', async () => { + it('Hell segment is aria-checked="true" in light mode', async () => { render(ThemeToggle); - const btn = await page.getByRole('button').element(); - expect(btn.getAttribute('title')).toBe(btn.getAttribute('aria-label')); + await expect + .element(page.getByRole('radio', { name: 'Hell' })) + .toHaveAttribute('aria-checked', 'true'); + await expect + .element(page.getByRole('radio', { name: 'Dunkel' })) + .toHaveAttribute('aria-checked', 'false'); }); }); -describe('ThemeToggle — label derivation (dark mode)', () => { +describe('ThemeToggle — renders segments (dark mode)', () => { beforeEach(() => { localStorage.setItem('theme', 'dark'); }); - it('aria-label invites switching to light mode when theme is dark', async () => { + it('Dunkel segment is aria-checked="true" in dark mode', async () => { render(ThemeToggle); - const btn = await page.getByRole('button').element(); - expect(btn.getAttribute('aria-label')).toBe('Zu hellem Design wechseln'); - }); - - it('title equals aria-label in dark mode', async () => { - render(ThemeToggle); - const btn = await page.getByRole('button').element(); - expect(btn.getAttribute('title')).toBe(btn.getAttribute('aria-label')); + await expect + .element(page.getByRole('radio', { name: 'Dunkel' })) + .toHaveAttribute('aria-checked', 'true'); + await expect + .element(page.getByRole('radio', { name: 'Hell' })) + .toHaveAttribute('aria-checked', 'false'); + }); +}); + +describe('ThemeToggle — theme switching', () => { + beforeEach(() => { + localStorage.setItem('theme', 'light'); + }); + + it('clicking Dunkel sets data-theme=dark on documentElement', async () => { + render(ThemeToggle); + await page.getByRole('radio', { name: 'Dunkel' }).click(); + expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); + }); + + it('clicking Dunkel persists theme in localStorage', async () => { + render(ThemeToggle); + await page.getByRole('radio', { name: 'Dunkel' }).click(); + expect(localStorage.getItem('theme')).toBe('dark'); }); });