--- phase: 11-ui-enhancements plan: "03" type: execute wave: 2 depends_on: - "11-01" files_modified: - imptune/templates/base.html - tests/test_static.py - tests/e2e/test_theme_toggle.py - tests/e2e/test_i18n_toggle.py autonomous: true requirements: - UIE-04 - UIE-05 must_haves: truths: - "A theme toggle button is visible on every page in the top-right area" - "Clicking the theme button cycles data-theme on through light -> dark -> auto" - "The chosen theme persists across page reloads (stored in localStorage)" - "A FR/EN toggle is visible in the top-right area alongside the theme button" - "Clicking the language toggle switches all static UI labels (nav items, buttons, headings) between French and English" - "The chosen language persists across page reloads (stored in localStorage)" artifacts: - path: "imptune/templates/base.html" provides: "Top-right controls with theme + language toggles, Alpine.js stores" contains: "Alpine.store" - path: "tests/e2e/test_theme_toggle.py" provides: "E2E: theme button cycles data-theme, localStorage persists" min_lines: 20 - path: "tests/e2e/test_i18n_toggle.py" provides: "E2E: lang toggle switches nav labels, localStorage persists" min_lines: 20 key_links: - from: "base.html alpine:init script" to: "Alpine.store('theme') + Alpine.store('i18n')" via: "document.addEventListener('alpine:init', ...) before Alpine defer load" pattern: "alpine:init" - from: "Alpine.store('theme').cycle()" to: "document.documentElement.setAttribute('data-theme', ...)" via: "Alpine store method called on button click" pattern: "data-theme" - from: "nav links in base.html" to: "Alpine.store('i18n').t('key')" via: "x-text binding on each nav link and button" pattern: "\\$store\\.i18n\\.t" --- Add theme toggle (Light/Dark/System) and FR/EN language toggle to the global layout, entirely in base.html using Alpine.js stores. Purpose: UIE-04 + UIE-05 — Users need persistent theme preference and bilingual support. Both features live in base.html with Alpine.js $store — zero new backend routes, zero new dependencies. Output: Updated base.html with top-right controls, Alpine.js theme + i18n stores, E2E tests for both toggles. @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/11-ui-enhancements/11-CONTEXT.md @.planning/phases/11-ui-enhancements/11-RESEARCH.md Current base.html structure (full file): ```html ImpTune
{% block content %}{% endblock %}
``` Alpine.js store + alpine:init pattern (from RESEARCH.md): ```javascript document.addEventListener('alpine:init', () => { Alpine.store('theme', { ... }); Alpine.store('i18n', { ... }); }); ``` This script MUST run BEFORE alpine.min.js `defer` executes. Place the script tag before the ` ``` **Step 2 — Add top-right controls area to the layout in base.html:** Inside the `
`, add a top-right controls bar above the main content area. Modify the layout to include a controls area: ```html
{% block content %}{% endblock %}
``` Note on x-data: Since the buttons use $store (global), they need Alpine to be active. Each button element gets a minimal `x-data` attribute (empty string is fine) to be scoped into Alpine. Alternatively wrap the .topbar-controls div with x-data. **Step 3 — Add minimal CSS for topbar to imptune/static/app.css (if needed):** The topbar does not need app.css changes for basic functionality — Pico CSS handles button styles. BUT if the layout currently uses CSS grid/flex that doesn't accommodate the new .main-wrapper and .topbar, add minimal styles. Check existing app.css first. If .layout is a CSS grid with sidebar + main-content columns, wrap main-content in main-wrapper and update the grid to target .main-wrapper. Keep app.css changes minimal. NOTE: Do not modify app.css if it would break existing tests. The test_no_cdn_urls_in_templates test only checks HTML, not CSS. **Step 4 — Add integration test to tests/test_static.py:** Add function: ```python def test_theme_toggle_present(client): """GET / contains a theme toggle button (data-theme cycling control).""" response = client.get("/") assert response.status_code == 200 # The button's @click should reference $store.theme.cycle assert "theme" in response.text assert "cycle" in response.text or "store.theme" in response.text ``` cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/test_static.py -x -q -k "theme_toggle_present" 2>&1 | tail -10 Also: pytest tests/ -x -q --ignore=tests/e2e (full non-E2E suite GREEN) - base.html contains Alpine.js store definitions (theme + i18n) - Theme toggle button and FR/EN button visible on layout - test_theme_toggle_present passes - test_no_cdn_urls_in_templates still passes (no external URLs added) - All non-E2E tests GREEN Task 2: E2E tests for theme toggle and language toggle tests/e2e/test_theme_toggle.py, tests/e2e/test_i18n_toggle.py **Step 1 — Create tests/e2e/test_theme_toggle.py:** ```python """UIE-04: E2E tests for theme toggle — data-theme cycling and localStorage persistence.""" from __future__ import annotations import pytest def test_theme_cycles_on_click(page, live_server: str) -> None: """Clicking theme button cycles data-theme attribute: auto -> light -> dark -> auto.""" page.goto(f"{live_server}/", wait_until="domcontentloaded") # Initial state: auto (default from base.html) initial_theme = page.evaluate("document.documentElement.getAttribute('data-theme')") assert initial_theme == "auto" # Click once -> light page.click("button[aria-label='auto']") page.wait_for_function( "document.documentElement.getAttribute('data-theme') === 'light'", timeout=2000, ) assert page.evaluate("document.documentElement.getAttribute('data-theme')") == "light" # Click again -> dark page.click("button[aria-label='light']") page.wait_for_function( "document.documentElement.getAttribute('data-theme') === 'dark'", timeout=2000, ) assert page.evaluate("document.documentElement.getAttribute('data-theme')") == "dark" def test_theme_persists_across_reload(page, live_server: str) -> None: """After clicking theme toggle, the chosen theme is restored on reload.""" page.goto(f"{live_server}/", wait_until="domcontentloaded") # Switch to light mode page.click("button[aria-label='auto']") page.wait_for_function( "document.documentElement.getAttribute('data-theme') === 'light'", timeout=2000, ) # Reload the page page.reload(wait_until="domcontentloaded") # Theme should still be light (from localStorage) theme_after_reload = page.evaluate("document.documentElement.getAttribute('data-theme')") assert theme_after_reload == "light" # Cleanup: reset to auto page.evaluate("localStorage.setItem('imptune_theme', 'auto')") ``` **Step 2 — Create tests/e2e/test_i18n_toggle.py:** ```python """UIE-05: E2E tests for language toggle — FR/EN switching and localStorage persistence.""" from __future__ import annotations import pytest def test_language_toggle_switches_nav_label(page, live_server: str) -> None: """Clicking FR/EN button switches nav label from French to English.""" page.goto(f"{live_server}/", wait_until="domcontentloaded") # Default lang is 'fr' — nav should show French labels # Wait for Alpine to hydrate page.wait_for_function( "document.querySelector('nav a[href=\"/printers\"]').textContent.trim() !== ''", timeout=3000, ) # In French, printers nav label = 'Imprimantes' printers_label_fr = page.text_content("nav a[href='/printers']").strip() assert printers_label_fr == "Imprimantes", f"Expected 'Imprimantes', got '{printers_label_fr}'" # Click the language toggle button page.click("button[title='Toggle language']") # Wait for label to update page.wait_for_function( "document.querySelector('nav a[href=\"/printers\"]').textContent.trim() === 'Printers'", timeout=2000, ) printers_label_en = page.text_content("nav a[href='/printers']").strip() assert printers_label_en == "Printers" def test_language_persists_across_reload(page, live_server: str) -> None: """After switching to EN, language is preserved on page reload.""" page.goto(f"{live_server}/", wait_until="domcontentloaded") # Switch to English page.click("button[title='Toggle language']") page.wait_for_function( "document.querySelector('nav a[href=\"/printers\"]').textContent.trim() === 'Printers'", timeout=2000, ) # Reload page.reload(wait_until="domcontentloaded") page.wait_for_function( "document.querySelector('nav a[href=\"/printers\"]').textContent.trim() !== ''", timeout=3000, ) label_after_reload = page.text_content("nav a[href='/printers']").strip() assert label_after_reload == "Printers" # Cleanup: reset to fr page.evaluate("localStorage.setItem('imptune_lang', 'fr')") ``` Note on E2E test selectors: these tests use `button[aria-label='auto']` for theme and `button[title='Toggle language']` for i18n. These selectors must match what Task 1 renders in base.html. Verify the button attributes in the template match the test selectors. If different approaches were chosen in Task 1 (e.g., different aria-label strategy), update the selectors to match. cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/e2e/test_theme_toggle.py tests/e2e/test_i18n_toggle.py -x -q 2>&1 | tail -15 - test_theme_cycles_on_click: data-theme cycles auto -> light -> dark on button clicks - test_theme_persists_across_reload: theme persists after page reload - test_language_toggle_switches_nav_label: nav label switches from Imprimantes to Printers on toggle - test_language_persists_across_reload: language choice persists after reload All 4 E2E tests GREEN. Run full non-E2E suite: ``` cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/ -x -q --ignore=tests/e2e ``` Expected: all GREEN (including test_no_cdn_urls_in_templates — no external URLs in base.html). Run E2E for this plan: ``` cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/e2e/test_theme_toggle.py tests/e2e/test_i18n_toggle.py -q ``` Expected: 4 tests GREEN. Manual spot-check (checkpoint:human-verify handled by /gsd:verify-work): - Open any page — theme button and FR/EN button visible in top-right area - Click theme button — dark mode activates (background goes dark) - Reload — dark mode persists - Click FR/EN — nav labels switch language - Reload — language persists - Theme toggle button visible on all pages; cycles data-theme: auto -> light -> dark -> auto - Chosen theme persists across page reloads (localStorage key: imptune_theme) - FR/EN toggle button visible alongside theme button; all nav labels, heading labels switch language - Chosen language persists across page reloads (localStorage key: imptune_lang) - test_theme_toggle_present (integration) GREEN - All 4 E2E tests GREEN (theme cycles, theme persists, lang switches, lang persists) - test_no_cdn_urls_in_templates still GREEN (no CDN URLs added) After completion, create `.planning/phases/11-ui-enhancements/11-03-SUMMARY.md`