Theme system with light and dark themes (#19) #59
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!59
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "19-theme-system"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #19.
Summary
[data-theme="<id>"]selectors. Components reference variables only —app/page.tsxandapp/offline/page.tsxmigrated off hard-coded hex colours.app/themes/registry.ts. Both theThemeIdtype and theisThemeId/isThemePreferencevalidators derive fromTHEMES, so adding a theme is a real data-only change. Shipslightanddark(both with blue accents).<script>in<head>(app/themes/theme-init-script.tsx) that reads thecarol_themecookie and sets<html data-theme="…">synchronously before paint. The script's allowlist is generated fromALL_PREFERENCES, so it can't drift from the registry. The root layout stays a non-async server component so/offline'sforce-static(Serwist's precache target) is preserved.user_settingstable (db/schema.ts+ migration 003).user_idis the PK + FK; one row per user. Today onlythemelives here — future settings extend the same row with additive columns.GET/PATCH /api/settings. Default-deny via the proxy; no allowlist entry needed. PATCH validates, upserts, and sets thecarol_themecookie on the response.app/api/auth/login/route.ts) and/api/auth/mehydrate thecarol_themecookie from the saved preference. New devices and cross-device theme changes flow through this path.app/components/theme-switcher.tsx— client component, three radio buttons (Auto + each registered theme), usesuseSyncExternalStoreto bridge<html data-theme>cleanly under the React-Compiler strict rules. Mounted on/for v1; #20 will move it into a nav/settings surface./offline.What didn't change
/offlinestill shows as○ (Static)in the build output — verified./api/settingsis gated by the proxy by default.Acceptance criteria
<html data-theme>synchronously on click anduseSyncExternalStorere-renders the radio group in the same tick.tests/api/settings.test.ts(login sets carol_theme to the user's saved theme) and by manual probe:PATCH /api/settings {theme:"dark"}→ log out → log back in → responseSet-Cookie: carol_theme=dark.Demonstrating AC #2 — adding a
foresttheme14 lines total. Type + validator + switcher UI + inline-init-script allowlist all update mechanically. Not encoded as a CI test — too fragile to assert on line count — but the registry test does assert that the init script's allowlist is generated from
ALL_PREFERENCESrather than hard-coded, which is the load-bearing invariant.Test plan
npm run typecheck— clean.npm run lint— clean (after restructuring the switcher arounduseSyncExternalStoreto satisfyreact-hooks/set-state-in-effectandreact-hooks/immutability).npm test— 86 passed / 24 skipped (110 total). 25 new tests acrosstests/db/user-settings.test.ts(dual-engine),tests/api/settings.test.ts(HTTP layer + cookie hydration on login/me),tests/themes/registry.test.ts(validators + script-allowlist consistency).npm run build— succeeds;/offlinestill○ (Static),/api/settingsshows asƒ, Proxy (Middleware) intact.npm run dev— clean start, inline init script + theme CSS present in<head>, all routes respond as expected.GET /api/settingsunauth → 401, authed (no row) →{"theme":"auto"},PATCH {"theme":"dark"}→ 200 +Set-Cookie: carol_theme=dark,PATCH {"theme":"neon"}→ 400invalid_theme, second login → response includesSet-Cookie: carol_theme=dark.prefers-color-schemeflips.🤖 Generated with Claude Code
e2ccc87c0c71e22f5d0f