#31 — Appearance Settings (Dark Mode) #31
Labels
No labels
priority/could
priority/must
priority/should
priority/wont
status/blocked
status/claimed
status/done-migrated
type/bug
type/feature
type/infra
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/todo#31
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Story: Appearance Settings (Dark Mode)
As a user,
I want to switch the app between Light, Dark, and System theme,
so that it is comfortable to use in different lighting conditions and matches my OS preference.
Acceptance criteria:
prefers-color-schemesetting)Out of scope for this story:
Blockers: None (the Appearance tab placeholder already exists from
#07)Priority: Could — high satisfaction relative to effort; the tab placeholder is already in place.
Status: Delivered.
:roothad been a byte-for-byte duplicate of.darksince the originalUI redesign (
#42) — the app had never actually had a working light theme. This story wrote agenuine light OKLCH palette (kept the orange-500 primary/ring accent consistent across both
themes) and added a
--successtoken (mirroring the existing--destructivelight/dark split)after code review found
text-green-400success messages, unchanged since before this story butnever previously exposed to a light background, failed WCAG AA (~1.7:1) against the new white
default.
Backend:
Common.Types.Theme(plain enum —TodoListColor/TodoPriorityprecedent, not a VogenVO) stored as a
stringcolumn onUserEntity;GetThemePreferenceQuery/UpdateThemePreferenceCommandmirror theIsEmailVerifiedsingle-field query/command shape.Frontend:
theme.tsresolvesSystemviaprefers-color-schemeand toggles the.darkclassalready scaffolded by
#42's@custom-variant dark; a blocking inline script inindex.htmlmirrors that resolution logic to apply the cached choice before first paint (avoiding a flash of
the wrong theme), reconciled against the server once the app mounts.
/code-review(high effort, 8 finder angles + a verification pass) found and fixed: a real racewhere the mount-time
GetThemePreferenceQueryfetch could resolve after a manual Settings changeand silently revert it (fixed with a
themeVersioncounter checked before applying a staleresponse);
localStorage's theme key being device- not account-scoped, so a shared/public browserwould flash the previous account's theme on the next login (fixed by resetting to
Systemwherever
setUserId(null)fires — the one place every logout/session-loss path, including the403 handler, already funnels through); and the contrast regression above.
store.ts'ssetThemenow applies the theme itself, folding away the manually-paired
setTheme+applyThemecall sitesthe review flagged as an unenforced-pairing risk.
Verified live end-to-end (register, toggle Dark/Light with immediate visual effect, confirm the
.darkclass is present atdomcontentloadedon reload with no FOUC, confirm logout resets thetheme) against the sandbox's
db/redisnetwork via a throwaway Playwright script — seeai/roles/memory/03_backend_engineer_memory.md's#27entry for the Docker-free live-verificationtechnique this reused. Backend unit tests for the two new handlers are Testcontainers-backed and
can't run in this Docker-less sandbox, but compile clean and will run for real on the next green
CI pass.