Severity: Medium.From a frontend UX audit. Decide-and-commit.
Problem
web/src/index.css:53-72 defines a complete .dark token set and the ui/* components carry dark: variants (button.tsx, input.tsx, badge.tsx, checkbox.tsx). But (a) nothing ever applies the .dark class and there is no theme toggle (grep → no setTheme/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:
Ship it: add a theme toggle (persisted, with a system-preference default) that toggles .dark on <html>. Completing the token migration (see the design-system issue) makes most of the app adapt automatically.
Drop it: remove the .dark token block and the dark: variants until it's actually wanted.
Source: frontend UX/design audit, 2026-06-06.
**Severity: Medium.** _From a frontend UX audit. Decide-and-commit._
## Problem
`web/src/index.css:53-72` defines a complete `.dark` token set and the `ui/*` components carry `dark:` variants (`button.tsx`, `input.tsx`, `badge.tsx`, `checkbox.tsx`). But (a) nothing ever applies the `.dark` class and there is **no theme toggle** (grep → no `setTheme`/`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:
- **Ship it:** add a theme toggle (persisted, with a system-preference default) that toggles `.dark` on `<html>`. Completing the token migration (see the design-system issue) makes most of the app adapt automatically.
- **Drop it:** remove the `.dark` token block and the `dark:` 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:
Tri-state toggle (Light / Dark / System) — an icon segmented control (lucide Sun/Moon/Monitor) in the header next to the language switch, with aria-pressed + aria-label. Default is System.
Framework-free core (web/src/theme/theme.ts): resolveTheme/readTheme/applyTheme toggling .dark on <html>, all DOM globals guarded.
useTheme hook (web/src/theme/use-theme.ts): persists to localStorage, and while in System mode subscribes to prefers-color-scheme so the app live-tracks the OS until the user pins a value (listener torn down when pinned).
No FOUC: a synchronous inline <script> in index.html applies the resolved class before first paint; its logic matches theme.ts exactly (no double-apply flash).
Dark --primary contrast: nudged dark --primary/--ring to oklch(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/config field), 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_theme on ConfigView, mirroring default_language); cross-tab sync via a storage listener; a dedicated dark-mode visual QA pass across every screen.
Shipped — merged to `main` (`9323c60`). Took the "ship it" path (the #49 token migration made the screens dark-ready).
**What landed:**
- **Tri-state toggle** (Light / Dark / System) — an icon segmented control (lucide Sun/Moon/Monitor) in the header next to the language switch, with `aria-pressed` + `aria-label`. Default is **System**.
- **Framework-free core** (`web/src/theme/theme.ts`): `resolveTheme`/`readTheme`/`applyTheme` toggling `.dark` on `<html>`, all DOM globals guarded.
- **`useTheme` hook** (`web/src/theme/use-theme.ts`): persists to `localStorage`, and while in System mode subscribes to `prefers-color-scheme` so the app **live-tracks the OS** until the user pins a value (listener torn down when pinned).
- **No FOUC**: a synchronous inline `<script>` in `index.html` applies the resolved class before first paint; its logic matches `theme.ts` exactly (no double-apply flash).
- **Dark `--primary` contrast**: nudged dark `--primary`/`--ring` to `oklch(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/config` field), **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_theme` on `ConfigView`, mirroring `default_language`); cross-tab sync via a `storage` listener; a dedicated dark-mode visual QA pass across every screen.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.