feat(ui): light/dark/system theme switching + per-user pref (LC-191) #237

Merged
nrupard merged 1 commit from feat/lc-191-theme-switching into main 2026-05-27 20:57:19 +02:00
Owner

Theme switching on top of the LC-190 token foundation: light / dark / system, a per-user server-side preference, and a no-flash client bootstrap.

What lands

  • Per-user pref: users.theme column (auth migration 0025, mirroring users.locale) - system/light/dark, NULL = system. Threaded through UserRecord/User + set_user_theme; 0025 added to the 22 hand-rolled auth-pool test files (migration-list drift, per CLAUDE.md).
  • Dark palette: [data-theme="dark"] in main.css (slate-based, color-scheme: dark) using the LC-190 token contract.
  • No-flash bootstrap (base.html): a synchronous script placed before the stylesheets sets <html data-theme> from the lc-theme cookie -> localStorage -> prefers-color-scheme, before CSS paints. Keeps <meta theme-color> in step, follows OS changes while on system, and exposes window.__lcSetTheme for the picker.
  • Cross-device sync: the resolve_locale middleware stamps the lc-theme cookie from users.theme when it drifts, so a fresh device's first authed response sets the cookie the bootstrap reads.
  • Settings -> Appearance: System/Light/Dark picker - the inline onchange applies instantly via __lcSetTheme + remembers on-device; the form POST (/settings/appearance) persists server-side. User::theme_or_system() marks the selected option.

Scope note

The dark palette recolors the whole app once LC-193 migrates templates onto the tokens. Until then the switch recolors the <body> and any already-tokenized surfaces; un-migrated panels still use raw slate/white. So dark mode is functional-but-partial on this branch by design (the chain is LC-190 -> LC-191 -> LC-193). Merge order is yours; if a polished dark mode should land all at once, hold this until LC-193.

Verification

just test + just test-saas green; cargo fmt --all clean; i18n_catalog gate passes (new settings-appearance / theme-* keys in en + es main.ftl). Bootstrap is synchronous + pre-stylesheet (no flash); cookie is SameSite=Lax, 1-year, path=/.

Theme switching on top of the LC-190 token foundation: light / dark / system, a per-user server-side preference, and a no-flash client bootstrap. ## What lands - **Per-user pref:** `users.theme` column (auth migration 0025, mirroring `users.locale`) - `system`/`light`/`dark`, NULL = system. Threaded through `UserRecord`/`User` + `set_user_theme`; 0025 added to the 22 hand-rolled auth-pool test files (migration-list drift, per CLAUDE.md). - **Dark palette:** `[data-theme="dark"]` in `main.css` (slate-based, `color-scheme: dark`) using the LC-190 token contract. - **No-flash bootstrap** (`base.html`): a synchronous script placed **before the stylesheets** sets `<html data-theme>` from the `lc-theme` cookie -> localStorage -> `prefers-color-scheme`, before CSS paints. Keeps `<meta theme-color>` in step, follows OS changes while on system, and exposes `window.__lcSetTheme` for the picker. - **Cross-device sync:** the `resolve_locale` middleware stamps the `lc-theme` cookie from `users.theme` when it drifts, so a fresh device's first authed response sets the cookie the bootstrap reads. - **Settings -> Appearance:** System/Light/Dark picker - the inline `onchange` applies instantly via `__lcSetTheme` + remembers on-device; the form POST (`/settings/appearance`) persists server-side. `User::theme_or_system()` marks the selected option. ## Scope note The dark palette recolors the whole app **once LC-193 migrates templates onto the tokens**. Until then the switch recolors the `<body>` and any already-tokenized surfaces; un-migrated panels still use raw `slate`/`white`. So dark mode is functional-but-partial on this branch by design (the chain is LC-190 -> LC-191 -> LC-193). Merge order is yours; if a polished dark mode should land all at once, hold this until LC-193. ## Verification `just test` + `just test-saas` green; `cargo fmt --all` clean; `i18n_catalog` gate passes (new `settings-appearance` / `theme-*` keys in en + es `main.ftl`). Bootstrap is synchronous + pre-stylesheet (no flash); cookie is `SameSite=Lax`, 1-year, path=/.
feat(ui): light/dark/system theme switching + per-user pref (LC-191)
All checks were successful
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Kingfisher (push) Successful in 8s
check-secrets / Nosey parker (pull_request) Successful in 6s
check-secrets / TruffleHog (pull_request) Successful in 8s
check-secrets / Kingfisher (pull_request) Successful in 11s
Check / clippy + fmt + tests (pull_request) Successful in 4m12s
Create release / Create release from merged PR (pull_request) Has been skipped
745a33e087
Builds on the LC-190 token foundation. Adds runtime theme selection with a per-user server-side preference and a no-flash client bootstrap.

- users.theme column (auth migration 0025, mirroring users.locale): "system"/"light"/"dark", NULL = system. Threaded through UserRecord/User + set_user_theme; 0025 added to the 22 hand-rolled auth-pool test files.
- main.css: [data-theme="dark"] palette (slate-based, color-scheme: dark) using the LC-190 token contract. Applies app-wide once LC-193 migrates templates onto the tokens; until then the switch recolors the body + any already-tokenized surfaces.
- base.html: synchronous no-flash bootstrap placed BEFORE the stylesheets - sets <html data-theme> from the lc-theme cookie, else localStorage, else prefers-color-scheme, before CSS paints; keeps <meta theme-color> in step; follows OS changes while on system; exposes window.__lcSetTheme for the picker.
- resolve_locale middleware: stamps the lc-theme cookie from users.theme when it drifts, so the preference carries cross-device (a fresh device's first authed response sets the cookie the bootstrap reads).
- Settings -> Appearance: System/Light/Dark picker (instant via __lcSetTheme + POST /settings/appearance persists); User::theme_or_system() marks the selected option.

just test + just test-saas green; cargo fmt --all clean; i18n_catalog gate passes (new appearance keys in en/es main.ftl).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-191-theme-switching 2026-05-27 20:57:19 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!237
No description provided.