[feat-020] Dark mode + semantic-token migration across the frontend
Adds a proper light/dark/system theme to Olava. Users pick their mode
in /account settings; choice persists in localStorage. The work is
mostly a sweep - not adding dark variants to existing classes, but
migrating ~80 components from raw colour classes (`bg-white`,
`text-gray-700`, `border-gray-200`, etc.) to the semantic tokens that
were already defined in globals.css (`bg-card`, `text-foreground`,
`border-border`, etc.). Both light and dark token sets already
existed; the surface area was just hard-coded to light.
Infrastructure:
- contexts/ThemeContext.tsx - `system | light | dark`, listens to OS
prefers-color-scheme on system mode, applies/removes `dark` class
on <html>.
- layout.tsx - pre-hydration <script> runs before React mounts; reads
localStorage('olava.theme'), resolves system preference, sets
className=dark + style.colorScheme. Avoids flash of light theme.
- providers.tsx - ThemeProvider wraps the app inside Auth/UserProfile.
- components/shared/ThemeToggle.tsx - 3-way segmented control
(System / Light / Dark) with lucide icons.
- (pages)/account/page.tsx - new "Appearance" section hosting the
toggle. Account page itself migrated to semantic tokens.
Migration sweep (74 files, +1337/-1286):
- bg-white → bg-card; bg-gray-50/100 → bg-muted; bg-gray-200 → bg-secondary;
bg-gray-700/800/900 → bg-foreground; bg-gray-300 → bg-muted-foreground/30;
bg-gray-400 → bg-muted-foreground/60; bg-black → bg-primary
- text-gray-900/800/700 → text-foreground; text-gray-600/500 →
text-muted-foreground; text-gray-400 → text-muted-foreground/70;
text-gray-300/200 → text-muted-foreground/50 / 30; text-black →
text-foreground; text-white → text-primary-foreground
- border-gray-100/200/300 → border-border; border-gray-50 →
border-border/50; border-gray-400 → border-input;
border-gray-700/900 → border-foreground; directional border-{l,r,t,b}-
variants mapped accordingly
- divide-gray-* → divide-border; ring-gray-* → ring-ring;
focus:border-gray-* → focus:border-ring;
hover:bg-gray-300 → hover:bg-muted-foreground/40
- placeholder-gray-* → placeholder:text-muted-foreground (Tailwind v4 syntax)
- Skeleton shimmer gradient `from-gray-200 via-gray-300 to-gray-200` →
`from-muted via-muted-foreground/20 to-muted`
- Coloured-bg buttons (bg-{red,blue,green,emerald,amber,destructive}-*)
reverted to `text-white` so legibility holds in both themes - sed
initially mass-mapped them to `text-primary-foreground` which inverts
in dark and disappears on a saturated background.
Brand colours kept as-is: `bg-blue-{50..700}`, `text-blue-600`, etc.
are already CSS-variable-backed in globals.css `@theme inline` and
read fine on both themes. Status reds/greens/ambers (toasts, flag pills
in tabular cells, signup/login validation) also kept - they're meant
to stand out, not blend.
Verified: tsc --noEmit clean. 0 raw `gray-N` / `bg-white` / `bg-black`
/ `text-white` / `text-black` references remain in src/.
Defaults: new users land on `system` mode (follows OS). Existing users
with no localStorage land on `system` too. Toggle is in /account.
Known limitation: choice is per-device. DB-synced cross-device theme
is a small follow-up (add `theme` column to user_profiles, sync on
mode change, hydrate at login) if it becomes worth it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| Repository | nwhitehouse/mike |
|---|---|
| Author | Nick Whitehouse <nick.whitehouse@mccarthyfinch.com> |
| Authored | |
| Parents | eaef8912 |
| Stats | 74 files changed , +1505 , -1283 |
| Part of | Dark mode + semantic-token migration |
Capture this commit into my fork
Download a Markdown prompt that tells Claude how to port this
exact commit into your working tree. Run it via
claude -p < capture-commit-88c73cbe.md
from inside the repo you want the change in.