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:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user