nwhitehouse ships dark mode the right way

A fork-wide theme overhaul that swaps hard-coded colours for reusable tokens - so dark mode comes almost for free.

chat-uibranding

nwhitehouse added a three-way theme switch (system, light, dark) to their Mike fork, with the toggle living in a new Appearance section on the account page. The real work, though, isn't the toggle - it's the cleanup behind it. Roughly 80 components were rewritten to stop referring to specific colours like "white" or "gray" and instead use named roles like "card" or "muted text". Once a component talks about roles, the theme system can swap the palette underneath it without touching the component again.

A small second pass cleaned up the bits that slipped through - the Onit logo on dark backgrounds, an invisible black send button in the chat box. The fix is a tidy SVG-swapping trick that works even on server-rendered pages.

So what Anyone running a Mike fork who wants dark mode should lift this wholesale - it's the pattern that scales, rather than a one-off retrofit.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

2 commits from nwhitehouse/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
88c73cbe [feat-020] Dark mode + semantic-token migration across the frontend Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
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>
3d3a2991 [feat-020] Dark-mode polish: white Onit mark + light send button Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
Two visible-in-dark-mode fixes from initial smoke testing:

- Onit "O" mark swaps to ONIT_Mark_White.svg in dark mode. Added
  /public/onit-mark-white.svg and updated all 5 sites that render
  the dark mark (InitialView, AppSidebar, WorkflowList, site-logo,
  onit-status-icon) to use the dual-img pattern with `block
  dark:hidden` + `hidden dark:block`. No theme-context dependency
  so server-rendered components Just Work.

- Chat-input send button now flips to a white gradient with a
  dark arrow in dark mode. Was bg-gradient-to-b from-neutral-700
  to-black with text-primary-foreground - invisible on dark page
  background. Added `dark:from-neutral-100 dark:to-white
  dark:disabled:from-neutral-300 dark:disabled:to-neutral-200
  dark:border-foreground/20`. text-primary-foreground already
  flips correctly so the arrow goes white→dark with the theme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-151.md from inside the repo you want the changes in.

⬇ Download capture-thread-151.md