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.
This commit is contained in:
ginnoir
2026-05-06 00:56:17 -05:00
parent 453c75ea4c
commit 35a14b81c9
3 changed files with 93 additions and 1 deletions
+5 -1
View File
@@ -9,7 +9,11 @@ Living progress tracker. Update at the end of each task. The canonical brief is
## Next up
- **03 — Core modules** ([brief](docs/tasks/)). Calendar, lists, notes CRUD. Sequential — must finish 02 first.
- **03 — Drizzle + Postgres setup** ([brief](docs/tasks/03-drizzle-postgres.md)). Phase 1 is sequential through 08.
## Phase 1 remaining
- 03 Drizzle + Postgres → 04 module loader → 05 compose/Caddy → 06 Authentik OIDC → 07 household seed → **08 theming infrastructure** (newly added; multi-theme + per-user dark/light, must land before module work so module UIs adopt the token system from day one).
## How to resume in a fresh session
+87
View File
@@ -0,0 +1,87 @@
# 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.
+1
View File
@@ -32,6 +32,7 @@ Every task file has these sections:
- [05 — Compose stack & Caddy](05-compose-caddy.md)
- [06 — Authentik install + OIDC integration](06-authentik-oidc.md)
- [07 — Household seeding & session](07-household-seed.md)
- [08 — Theming infrastructure (multi-theme + dark mode)](08-theming.md)
### Phase 2 — Core modules