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.
3.9 KiB
3.9 KiB
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>:
: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)
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
systemmode, the resolved value comes from a small inline<script>in<head>that readsprefers-color-schemeandlocalStorage.themebefore paint — covers signed-out users (/login,/s/<token>) too.
Client hook + picker
useTheme()hook: returns{ theme, mode, setTheme, setMode }. Optimistically flips<html>attributes, writes tolocalStorage, and (if signed in) calls asetUserThemeserver 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 theTHEMESregistry 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: darkis honored whentheme_mode = 'system'.- Adding a third theme requires only: one CSS block in
globals.css+ one entry inTHEMES. 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/.darkCSS variables automatically — notailwind.configplumbing required. - Keep the inline pre-paint
<script>tiny (under ~30 lines). It runs before React hydrates so anything heavier is wrong.