Files
famapp/docs/tasks/08-theming.md
ginnoir 35a14b81c9 Add task 08 — theming infrastructure
Multi-theme × {light, dark} per-user theming, switchable on the fly.
Slots into phase 1 before modules start so the token system is the
foundation rather than a retrofit. Update STATUS to point at task 03
correctly and reflect 08 in the phase 1 roadmap.
2026-05-06 00:56:17 -05:00

88 lines
3.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 08 — Theming infrastructure (multi-theme + dark mode)
## Goal
Set up a CSS-variable-based theme system that supports multiple named themes × `{light, dark}`, persists per user, and switches on the fly without a flash of unstyled content.
## Why
Cheap to set up before modules exist, painful to retrofit afterwards. Locking in the structure now means every later component "just works" with theming, and adding a new theme is a CSS block — no code change.
## Depends on
- 02 (Next.js skeleton, shadcn already initialized)
- 07 (users table, so we can add columns)
## Scope
### Schema additions (new migration in `src/modules/_core/schema.ts`)
- `users.theme` — text, default `'default'`.
- `users.theme_mode` — text, default `'system'`, check constraint `('light', 'dark', 'system')`.
### CSS structure (`src/app/globals.css`)
Use shadcn's CSS-variable conventions, scoped by `data-theme` on `<html>`:
```css
:root { /* default light tokens */ }
.dark { /* default dark tokens */ }
[data-theme="warm"] { /* warm light */ }
[data-theme="warm"].dark { /* warm dark */ }
```
Ship at least **two** themes (`default` + one more) so the architecture is actually exercised. Token values can be placeholder — refining the palettes is a separate later concern.
### Theme registry (`src/modules/_core/themes.ts`)
```ts
export type ThemeMode = "light" | "dark" | "system";
export type ThemeId = "default" | "warm" | string;
export const THEMES: ReadonlyArray<{ id: ThemeId; label: string }> = [
{ id: "default", label: "Default" },
{ id: "warm", label: "Warm" },
];
```
Picker reads from this list — no hardcoded options in the UI.
### Root layout (server component)
- Reads the current session; pulls `theme` + `theme_mode`.
- Renders `<html data-theme={theme} className={resolvedMode === "dark" ? "dark" : ""}>` on first paint. **No flash.**
- For the `system` mode, the resolved value comes from a small inline `<script>` in `<head>` that reads `prefers-color-scheme` and `localStorage.theme` before paint — covers signed-out users (`/login`, `/s/<token>`) too.
### Client hook + picker
- `useTheme()` hook: returns `{ theme, mode, setTheme, setMode }`. Optimistically flips `<html>` attributes, writes to `localStorage`, and (if signed in) calls a `setUserTheme` server action to persist.
- `<ThemePicker />` component: theme `<select>` + light/dark/system segmented control. Mounted in `/settings`. Reusable.
### Server action
- `setUserTheme({ theme, mode })` updates the current user row. Validates against the `THEMES` registry and the mode whitelist.
## Out of scope
- Final color palettes — placeholder values are fine; you'll iterate visually later.
- High-contrast / accessibility-tuned variants (separate task if needed).
- Per-household theme.
- Animated transitions between themes (instant swap is fine).
- Theme preview thumbnails in the picker.
## Acceptance criteria
- [ ] On `/settings`, picking a theme and a mode applies instantly with no page reload.
- [ ] Reloading preserves the choice: signed in → from DB; signed out → from localStorage.
- [ ] First paint after sign-in matches the stored theme — verify by adding a noticeable contrast in the alt theme and reloading; no flash.
- [ ] `prefers-color-scheme: dark` is honored when `theme_mode = 'system'`.
- [ ] Adding a third theme requires only: one CSS block in `globals.css` + one entry in `THEMES`. No other code touched.
- [ ] Existing shadcn components render correctly across both themes × both modes (manual visual check is fine).
## Notes
- Don't introduce custom semantic tokens beyond what shadcn ships with until a real design need surfaces. The fewer tokens, the easier theming stays.
- Tailwind v4 picks up `:root` / `.dark` CSS variables automatically — no `tailwind.config` plumbing required.
- Keep the inline pre-paint `<script>` tiny (under ~30 lines). It runs before React hydrates so anything heavier is wrong.