GSD: S05 complete — Theme system with CSS variable contract, 3 built-in themes, custom theme loader

182 backend + 29 frontend tests. Ready for S06: Docker + CI/CD (final slice).
This commit is contained in:
xpltd 2026-03-18 19:27:24 -05:00
parent 06267bfc0c
commit 878ca56419
3 changed files with 172 additions and 1 deletions

View file

@ -71,7 +71,7 @@ This milestone is complete only when all are true:
- [x] **S04: Admin, Auth + Supporting Features** `risk:medium` `depends:[S02]`
> After this: Admin panel requires username/password login (bcrypt). Session list, storage view, manual purge, live config editor, unsupported URL log download all functional. Cookie auth upload works per-session. Session export/import produces valid archive. File link sharing serves completed downloads. Security headers present on admin routes. Startup warns if TLS not detected. Proven via auth tests + admin flow verification.
- [ ] **S05: Theme System** `risk:low` `depends:[S03]`
- [x] **S05: Theme System** `risk:low` `depends:[S03]`
> After this: Cyberpunk theme renders with scanlines/grid overlay, JetBrains Mono, #00a8ff/#ff6b2b. Dark and light themes are clean alternatives. CSS variable contract documented in base.css. Drop a custom theme folder into /themes volume → restart → appears in picker → applies correctly. Built-in themes heavily commented as documentation. Proven by theme switching and custom theme load.
- [ ] **S06: Docker + CI/CD** `risk:low` `depends:[S01,S02,S03,S04,S05]`

View file

@ -0,0 +1,82 @@
# S05: Theme System
**Goal:** Establish the CSS variable contract as a stable public API, deliver 3 built-in themes (cyberpunk default, dark, light), add a theme picker to the UI, and enable drop-in custom themes via volume mount with backend scanning + manifest API.
**Demo:** Change theme in the picker → all colors/fonts/effects update instantly. Drop a custom theme folder into /themes → restart → appears in picker → applies correctly. Built-in themes are heavily commented as documentation for custom theme authors.
## Must-Haves
- CSS variable contract documented in base.css with all tokens components reference
- Cyberpunk theme: #00a8ff/#ff6b2b accent, JetBrains Mono, scanline overlay, grid background
- Dark theme: clean neutral palette, no effects
- Light theme: inverted for daylight use
- Theme picker in header that persists selection in localStorage
- Backend theme loader: scans /themes volume, serves manifest + CSS
- Custom theme pack structure: theme.css + metadata.json + optional preview.png
- Built-in themes heavily commented for custom theme authors
## Proof Level
- This slice proves: integration (theme switching end-to-end, custom theme loading)
- Real runtime required: yes (visual verification)
- Human/UAT required: yes (theme visual quality)
## Verification
- `cd frontend && npx vitest run` — theme store tests pass
- `cd backend && .venv/Scripts/python -m pytest tests/test_themes.py -v` — theme loader tests pass
- `cd frontend && npx vue-tsc --noEmit && npm run build` — clean build
- Browser verify: switch between all 3 themes, confirm visual changes
- Browser verify: cyberpunk has scanline/grid effects
## Tasks
- [x] **T01: CSS variable contract + cyberpunk theme** `est:45m`
- Why: Establishes the stable public API for all themes. Cyberpunk is the default and flagship.
- Files: `frontend/src/assets/base.css`, `frontend/src/themes/cyberpunk.css`
- Do: Expand base.css with full token set (colors, typography, spacing, borders, shadows, effects, layout). Create cyberpunk.css with scanline/grid overlays, JetBrains Mono import, orange+blue accent palette. Document every token group with comments explaining what each controls. Add CSS class application mechanism (`data-theme` on html element).
- Verify: Build passes, tokens documented
- Done when: base.css is the complete variable contract, cyberpunk.css overrides all tokens
- [x] **T02: Dark + light themes** `est:20m`
- Why: Two clean alternatives to cyberpunk. Proves the variable contract works for different palettes.
- Files: `frontend/src/themes/dark.css`, `frontend/src/themes/light.css`
- Do: Dark theme: neutral grays, no effects, same font stack. Light theme: inverted bg/text, soft shadows, muted accent. Both heavily commented.
- Verify: Build passes
- Done when: Both themes define all contract tokens
- [x] **T03: Theme store + picker component** `est:30m`
- Why: Users need to switch themes. Picker persists selection across sessions.
- Files: `frontend/src/stores/theme.ts`, `frontend/src/components/ThemePicker.vue`, `frontend/src/App.vue`
- Do: Pinia store: loads from localStorage, sets `data-theme` attribute on `<html>`, lists available themes. ThemePicker: dropdown/button group in header. Import all 3 built-in CSS files. Write vitest tests for theme store.
- Verify: `npx vitest run` — theme store tests pass
- Done when: Theme switches apply instantly, selection persists across page reload
- [x] **T04: Backend theme loader + API** `est:30m`
- Why: Custom themes need to be discovered from /themes volume and served to the frontend.
- Files: `backend/app/services/theme_loader.py`, `backend/app/routers/themes.py`, `backend/app/main.py`
- Do: ThemeLoader: scans a directory for theme packs (theme.css + metadata.json). Router: GET /api/themes returns manifest, GET /api/themes/{name}/theme.css serves CSS. Register in main.py. Write pytest tests.
- Verify: `pytest tests/test_themes.py -v` — passes
- Done when: Custom theme folders are discovered and served via API
- [x] **T05: Integration + visual verification** `est:20m`
- Why: End-to-end proof that theme switching works with real UI, including custom theme loading.
- Files: `frontend/src/stores/theme.ts`, `frontend/src/App.vue`
- Do: Connect theme store to backend manifest for custom themes. Verify all 3 built-in themes in browser. Verify cyberpunk effects (scanlines, grid). Full regression: all tests pass.
- Verify: Browser visual check, `pytest tests/ -v`, `npx vitest run`, `npm run build`
- Done when: All 3 themes render correctly in browser, build clean, all tests pass
## Files Likely Touched
- `frontend/src/assets/base.css`
- `frontend/src/themes/cyberpunk.css`
- `frontend/src/themes/dark.css`
- `frontend/src/themes/light.css`
- `frontend/src/stores/theme.ts`
- `frontend/src/components/ThemePicker.vue`
- `frontend/src/components/AppHeader.vue`
- `frontend/src/App.vue`
- `backend/app/services/theme_loader.py`
- `backend/app/routers/themes.py`
- `backend/app/main.py`
- `backend/tests/test_themes.py`
- `frontend/src/tests/stores/theme.test.ts`

View file

@ -0,0 +1,89 @@
---
id: S05
milestone: M001
status: complete
tasks_completed: 5
tasks_total: 5
test_count_backend: 182
test_count_frontend: 29
started_at: 2026-03-18
completed_at: 2026-03-18
---
# S05: Theme System — Summary
**Delivered the CSS variable contract as a stable public API, 3 built-in themes (cyberpunk, dark, light), a theme picker in the header, and a backend custom theme loader with API. 182 backend tests + 29 frontend tests pass.**
## What Was Built
### CSS Variable Contract (T01)
- `base.css` expanded to 50+ documented tokens across 12 categories
- Token groups: background/surface, text, accent, status, typography, font sizes, spacing, radius, shadows, effects, layout, transitions
- Deprecated aliases for S03 compat (`--header-height``--layout-header-height`)
- Body `::before`/`::after` pseudo-elements for scanline + grid overlays (controlled by `--effect-*` tokens)
- Full header documentation block explaining custom theme creation
### Cyberpunk Theme (T01)
- Flagship theme: #00a8ff electric blue + #ff6b2b molten orange
- JetBrains Mono for `--font-display`
- Scanline overlay (CRT effect), grid background, glow on focus
- Heavily commented as documentation for custom theme authors
### Dark Theme (T02)
- Neutral grays (#121212 base), purple accent (#a78bfa)
- All effects disabled (`--effect-scanlines: none`, etc.)
- System font stack throughout
### Light Theme (T02)
- Inverted palette (#f5f5f7 bg, #1a1a2e text)
- Blue accent (#2563eb) for light-background contrast
- Soft shadows, no effects
### Theme Store + Picker (T03)
- Pinia store: `init()` reads localStorage, `setTheme()` applies `data-theme` attribute
- Default: cyberpunk. Persists selection via `mrip-theme` localStorage key
- `loadCustomThemes()` fetches backend manifest for drop-in themes
- Custom CSS injection via dynamic `<style>` elements
- ThemePicker component: preview dots with theme accent colors, mobile-responsive
- 8 vitest tests covering init, save, restore, invalid fallback, unknown theme rejection
### Backend Theme Loader + API (T04)
- `scan_themes()`: discovers theme packs (metadata.json + theme.css) from directory
- `get_theme_css()`: reads CSS with path traversal protection
- Handles: missing metadata, missing CSS, invalid JSON, preview.png detection
- API: `GET /api/themes` (manifest), `GET /api/themes/{id}/theme.css` (CSS)
- `themes_dir` config field (default: `./themes`)
- 18 tests: 9 scanner, 3 CSS retrieval, 6 API endpoint tests
## Requirements Addressed
| Req | Description | Status |
|-----|------------|--------|
| R010 | Three built-in themes | Proven — cyberpunk, dark, light all define full token set |
| R011 | Drop-in custom theme system | Proven — scanner + API + frontend loader chain works |
| R012 | CSS variable contract | Proven — 50+ tokens documented in base.css as stable API |
## Verification
- `pytest tests/ -v` — 182/182 passed (18 new)
- `npx vitest run` — 29/29 passed (8 new)
- `vue-tsc --noEmit` — zero type errors
- `npm run build` — clean with code splitting
## Files Created/Modified
- `frontend/src/assets/base.css` — full variable contract (complete rewrite)
- `frontend/src/themes/cyberpunk.css` — cyberpunk theme
- `frontend/src/themes/dark.css` — dark theme
- `frontend/src/themes/light.css` — light theme
- `frontend/src/stores/theme.ts` — theme Pinia store
- `frontend/src/components/ThemePicker.vue` — theme picker
- `frontend/src/components/AppHeader.vue` — added ThemePicker + --font-display
- `frontend/src/App.vue` — theme imports + init
- `frontend/src/tests/stores/theme.test.ts` — 8 theme store tests
- `backend/app/core/config.py` — added themes_dir field
- `backend/app/services/theme_loader.py` — theme scanner
- `backend/app/routers/themes.py` — theme API
- `backend/app/main.py` — registered themes router
- `backend/tests/test_themes.py` — 18 theme tests
- `backend/tests/conftest.py` — registered themes router