Frontend: dark mode is half-built dead code — wire a theme toggle or remove it #59
Reference in New Issue
Block a user
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?
Severity: Medium. From a frontend UX audit. Decide-and-commit.
Problem
web/src/index.css:53-72defines a complete.darktoken set and theui/*components carrydark:variants (button.tsx,input.tsx,badge.tsx,checkbox.tsx). But (a) nothing ever applies the.darkclass and there is no theme toggle (grep → nosetTheme/classList/next-themes), so dark mode can never activate; and (b) the feature screens use light-only literals (bg-neutral-50,text-neutral-400,bg-green-100, …) that wouldn't adapt anyway. Dark mode is currently maintenance overhead that can't turn on, and if it did, half the app would be unreadable.Suggested fix
Decide:
.darkon<html>. Completing the token migration (see the design-system issue) makes most of the app adapt automatically..darktoken block and thedark:variants until it's actually wanted.Source: frontend UX/design audit, 2026-06-06.
Shipped — merged to
main(9323c60). Took the "ship it" path (the #49 token migration made the screens dark-ready).What landed:
aria-pressed+aria-label. Default is System.web/src/theme/theme.ts):resolveTheme/readTheme/applyThemetoggling.darkon<html>, all DOM globals guarded.useThemehook (web/src/theme/use-theme.ts): persists tolocalStorage, and while in System mode subscribes toprefers-color-schemeso the app live-tracks the OS until the user pins a value (listener torn down when pinned).<script>inindex.htmlapplies the resolved class before first paint; its logic matchestheme.tsexactly (no double-apply flash).--primarycontrast: nudged dark--primary/--ringtooklch(0.72 0.18 277)— near-black button label now 6.7:1 (independently recomputed), comfortably AA. (Note: the parked "3.21:1" figure was a faulty measurement; the prior value already passed at ~5.7:1. Light mode unchanged.)Client-only (no
/api/configfield), no new npm dependency (lucide already present). en/sv parity; tests (theme core unit + ThemeSwitch interaction + story); gate green: typecheck, lint, 181 tests, build, check:size (184.4 KB gz), check:colors; no codename.Follow-ups (not in scope): per-account/server-synced theme default (
default_themeonConfigView, mirroringdefault_language); cross-tab sync via astoragelistener; a dedicated dark-mode visual QA pass across every screen.