# Theming & Dark Mode Alga PSA supports light/dark themes via `next-themes` with class-based switching. The `` element gets `.dark` or `.light`, activating CSS variable sets in `globals.css`. All colors flow through CSS custom properties referenced by Tailwind semantic tokens. ## Coding Standards ### Do 1. **Use Tailwind semantic tokens** — `bg-primary-50`, `text-foreground`, `border-border`, not raw colors 2. **Use CSS variables** for any custom styling — `rgb(var(--color-text-700))` not `#334155` 3. **Adapt dynamic colors** with `adaptColorsForDarkMode()` for user/entity-generated colors (tags, avatars, etc.) 4. **Use the hydration-safe pattern** when reading `resolvedTheme` in components — track a `mounted` state via `useEffect` and only read the theme after mount to avoid SSR mismatches 5. **Test both themes** — toggle dark mode and verify contrast, readability, borders 6. **Add dark overrides in `globals.css`** when integrating new third-party components 7. **Use `useAppTheme`** from `@alga-psa/ui/hooks` — not `useTheme` from next-themes directly (includes DB sync + preference persistence) ### Don't 1. **Hardcode hex/rgb** in components — won't respond to theme changes 2. **Use `bg-white`/`bg-black`** — use `bg-background` or `bg-card` (existing `bg-white` has a global override but new code should use tokens) 3. **Forget `suppressHydrationWarning`** on ``/`` if modifying root layout 4. **Import `useTheme` directly** from next-themes — use `useAppTheme` instead ### Common Mistakes **Wrong** — hardcoded background: ```tsx
``` **Fix** — use token: ```tsx
``` **Wrong** — inline color without dark variant: ```tsx ``` **Fix** — CSS variable: ```tsx ``` ## CSS Variable System Colors are space-separated RGB triples in `server/src/app/globals.css` under `.light`/`.dark`. Tailwind references them via `rgb(var(--color-*))` in `server/tailwind.config.ts` (`darkMode: 'class'`). In dark mode, 50-900 scales are **inverted** (low=dark, high=light), so `bg-primary-50` is always the subtlest background regardless of theme. **Token groups**: `--color-background`, `--color-card`, `--color-border-{50-900}`, `--color-text-{50-900}`, `--color-primary/secondary/accent-{50-900}`, `--color-status-success/warning/error`, `--badge-*` (semantic, branding-independent), `--color-sidebar-*`, `--alga-*` (semantic aliases). **Global dark overrides** in `globals.css` remap common utilities (`bg-white`, `text-gray-700`, `border-slate-200`) to CSS variables. Also overrides for react-day-picker, react-big-calendar, Tiptap/ProseMirror. ## Provider Chain ``` AppThemeProvider → ThemeBridge → BrandingProvider ``` | Provider | File | Role | |---|---|---| | `AppThemeProvider` | `server/src/components/providers/AppThemeProvider.tsx` | Wraps next-themes (`attribute="class"`, `defaultTheme="system"`, `disableTransitionOnChange`). Accepts optional `forcedTheme` prop to lock a specific theme (disables system detection). Injects DB persistence via `ThemeActionsProvider`. | | `ThemeBridge` | `server/src/components/providers/ThemeBridge.tsx` | Syncs `resolvedTheme` to Mantine (`forceColorScheme`) and Radix (`appearance`). Sets `data-theme` on ``. Hides content until mounted (FOUC prevention). | | `BrandingProvider` | `packages/tenancy/src/components/providers/BrandingProvider.tsx` | Generates CSS var overrides from tenant branding. `.dark {}` block with inverted shades. Checks for server-injected styles to avoid duplication. | Used in: MSP layout (`server/src/app/layout.tsx`), client portal (`server/src/app/client-portal/ClientPortalLayoutClient.tsx`), client auth (`server/src/app/auth/client-portal/layout.tsx`), MSP auth (`server/src/app/auth/layout.tsx` — with `forcedTheme="light"`). ## Theme Toggle & Persistence - **`ThemeToggle`** (`packages/ui/src/components/ThemeToggle.tsx`): Light/Dark/System dropdown. Returns `null` when flag disabled. IDs: `theme-toggle`, `data-automation-id="theme-toggle"`. - **`useAppTheme`** (`packages/ui/src/hooks/useAppTheme.tsx`): Extends next-themes with DB persistence + feature flag check. `lastSyncedTheme` ref prevents circular saves. **Flow**: Toggle → next-themes sets class → `useAppTheme` calls `updateThemePreferenceAction` → upserts `user_preferences` table (`setting_name='theme'`). On login, loads via `getThemePreferenceAction`. **Server actions** (`packages/users/src/actions/user-actions/themeActions.ts`): `getThemePreferenceAction` (withOptionalAuth), `updateThemePreferenceAction` (withAuth). ## Branding + Dark Mode Tenant colors override primary/secondary CSS vars. Dark mode inverts shade scales (`packages/tenancy/src/lib/generateBrandingStyles.ts`): `invertShades()` flips 50<->900, 100<->800, etc. (400/500 stay). FOUC prevention: server-side `