Files
famapp/docs/tasks/08-theming.md
T
ginnoir c73338e256 Code-side
src/lib/dev-login-config.ts — startup assertion: throws if NODE_ENV=production + ENABLE_DEV_LOGIN=true, scoped to runtime (skipped during next build).
Container

scripts/migrate.mjs — runs Drizzle migrations against DATABASE_URL.
deploy/docker-entrypoint.sh — runs migrations then exec node server.js. Skip with RUN_MIGRATIONS=false.
Dockerfile — copies drizzle/, scripts/migrate.mjs, entrypoint into runner stage; ENTRYPOINT now points at the script.
Compose

deploy/compose.yaml — famapp now image: ${FAMAPP_IMAGE:-ghcr.io/ginnoir/famapp:latest} (build still works locally as fallback). Authentik pinned via AUTHENTIK_IMAGE_TAG (default 2024.12.3). New RUN_MIGRATIONS env passed through.
.env.production.example — documents FAMAPP_IMAGE, AUTHENTIK_IMAGE_TAG, RUN_MIGRATIONS.
CI/CD

.github/workflows/ci.yml — push/PR: typecheck + lint + format:check + build.
.github/workflows/release.yml — v* tag: build + push ghcr.io/ginnoir/famapp:vX.Y.Z, :X.Y, :latest to GHCR.
Docs

deploy/README.md — full deploy/rollback/release runbook.
CHANGELOG.md — release log seeded with an Unreleased entry.
docs/tasks/09-pre-deploy-checklist.md — task 09 reframed from one-shot removal to a recurring pre-deploy checklist.
STATUS.md — updated.
Verified: pnpm typecheck, pnpm format, pnpm build, and docker compose config all clean.
2026-05-06 17:37:37 -05:00

96 lines
3.9 KiB
Markdown
Raw 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.