# Scratchpad — Dark Mode for Main Application - Plan slug: `dark-mode` - Created: `2026-02-09` ## What This Is Keep a lightweight, continuously-updated log of discoveries and decisions made while implementing this plan. ## Decisions - (2026-02-09) Two token systems exist: `--color-*` (main app, in globals.css) and `--alga-*` (ui-kit, in tokens.css). Need a mapping layer rather than replacing one — both are entrenched. - (2026-02-09) Theme switching uses CSS class on `` (`.light` / `.dark`) — this matches the existing globals.css structure and is compatible with Tailwind's `darkMode: 'class'`. - (2026-02-09) The ui-kit uses `data-theme="dark"` on `` — we'll need to set both the body class AND the data-theme attribute when switching. - (2026-02-09) **USE `next-themes`** — stable (<1KB, 6K+ stars, zero deps), handles SSR flash prevention, localStorage, system preference, and cross-tab sync. Replaces our broken `ThemeContext`. Hydration mismatch handled with `mounted` guard on toggle UI. - (2026-02-09) **PERSIST TO DB** — store theme preference in existing `user_preferences` table (`setting_name: 'theme'`). No new migration needed. `UserPreferences.upsert()` and existing REST API (`PUT /api/v1/users/[id]/preferences`) used for persistence. localStorage used as immediate cache; DB synced async for cross-device consistency. - (2026-02-09) **INCLUDE CLIENT PORTAL** — dark mode for both MSP and client portal in this effort. Client portal needs: add `ThemeProvider` to `ClientPortalLayoutClient.tsx`, add toggle to nav bar, fix `bg-gray-100` hardcoding, ensure `BrandingProvider` works in dark mode, cover auth pages. - (2026-02-09) **FIX DARK SUBMENU COLORS** — current `.dark` values (`--color-submenu-bg: #D0D5DD`, `--color-submenu-text: #000000`) are incorrect for dark mode. Fix to proper dark palette (e.g., `#1f2937` bg, `#f5f5f5` text). - (2026-02-09) **ADD BRANDING PREVIEW MODES** — client portal branding settings page gets a dark/light mode toggle in the preview panel so admins can see how branded colors look in both themes. - (2026-02-09) **AUTO-ADJUST BRANDED SHADES** — `BrandingProvider` will generate inverted shade scale for dark mode (swap 50↔900 etc.), matching the static primary/secondary inversion pattern in globals.css. No separate tenant dark-mode colors needed. - (2026-02-09) **TOGGLE FOR PREVIEW** — single toggle switch in branding settings, not side-by-side comparison. - (2026-02-09) **SELECTORS VERIFIED COMPATIBLE** — globals.css `.dark`/`.light` are bare class selectors (not `body.dark` or `html.dark`). They match `` (next-themes default target) correctly. CSS variables cascade to all descendants. No selector changes needed. - (2026-02-10) Installed `next-themes` in `server` workspace via npm workspaces to satisfy FR-1.1. - (2026-02-10) Enabled Tailwind dark mode via `darkMode: 'class'` in `server/tailwind.config.ts`. - (2026-02-10) Added theme-aware `--color-background`/`--color-card` CSS variables and switched Tailwind `background`/`card` colors to use them. - (2026-02-10) Added theme-aware status color CSS variables and wired Tailwind `success`/`warning`/`error` colors to them. - (2026-02-10) Replaced the light-locked ThemeContext with `next-themes` via `AppThemeProvider` in the root layout and removed the old context file. - (2026-02-10) Added `useAppTheme()` hook to load theme preference from `/api/v1/users/:id/preferences` and persist changes back to the DB. - (2026-02-10) Removed hardcoded `className="light"` from root `` so next-themes controls the theme class. - (2026-02-10) Added `ThemeBridge` to map resolved theme to Radix `appearance` and Mantine `forceColorScheme`. - (2026-02-10) Created `ThemeToggle` dropdown (Light/Dark/System) powered by `useAppTheme`. - (2026-02-10) Added `ThemeToggle` to the MSP header actions area. - (2026-02-10) Moved `ThemeToggle` and `useAppTheme` into `@alga-psa/ui` so both MSP and client portal can share them; added toggle to client portal nav. - (2026-02-10) next-themes provider now handles localStorage persistence and flash prevention without custom code. - (2026-02-10) useAppTheme now persists theme changes to `user_preferences` via the v1 preferences API. - (2026-02-10) useAppTheme loads the saved theme from `user_preferences` on auth and applies it. - (2026-02-10) Added CSS variable mapping to sync `--color-*` tokens to `--alga-*` tokens and set `data-theme` via `ThemeBridge` so ui-kit tokens track theme changes. - (2026-02-10) Updated iframe bridge to recompute and re-send theme tokens on theme changes (class/data-theme mutations). - (2026-02-10) Fixed dark submenu CSS variables to use dark background and light text. - (2026-02-10) Added dark-mode utility overrides for common Tailwind classes plus base body/header/main backgrounds to avoid white patches. - (2026-02-10) Added dark-mode form field styling for inputs, selects, and textareas to use theme-aware colors. - (2026-02-10) Replaced switch thumb hardcoded white with `--color-switch-thumb` and added checkbox/radio accent colors for dark mode. - (2026-02-10) Updated `time-slot-working` background to use theme-aware card color. - (2026-02-10) Replaced collaboration cursor hardcoded color with theme-aware text color variable. - (2026-02-10) Updated table hover and dotted background colors to use theme-aware CSS variables. - (2026-02-10) Marked common UI surfaces (buttons, tables, dialogs, dropdowns, tooltips, badges, tabs) as theme-aware based on shared tokenized styling and dark overrides. - (2026-02-10) Replaced hardcoded hex colors in dashboard/editor/ticket/project/billing CSS modules with CSS variable references. - (2026-02-10) Extension loading overlay already uses CSS variables, marked as dark-mode ready. - (2026-02-10) Added AppThemeProvider + ThemeBridge to client portal layout for theme class + data-theme sync. - (2026-02-10) Updated client portal layout background to use `bg-background` instead of hardcoded gray. - (2026-02-10) Added inverted branding shade generation and `.dark` scoped variables for both server-side and client branding injections. - (2026-02-10) Added client-portal auth layout to wrap sign-in/forgot-password flows with theme providers. - (2026-02-10) ThemeBridge now feeds Mantine color scheme, marking Mantine components as theme-aware. - (2026-02-10) Added dark-mode overrides for react-day-picker, react-big-calendar, and Tiptap/ProseMirror surfaces. - (2026-02-10) Marked client portal core pages as dark-ready after shared token and layout updates. - (2026-02-10) Marked MSP domain pages (tickets, projects, billing, scheduling, dashboard, settings) as dark-ready after shared overrides and CSS module updates. - (2026-02-10) Added branding preview dark/light toggle and scoped preview containers with `.dark` class. ## Discoveries / Constraints - (2026-02-09) `ThemeContext` (`server/src/context/ThemeContext.tsx`) is completely hardlocked to light — `setThemeStatus` ignores its argument and always sets `light`. This was intentional (comment says "Always set to light mode"). - (2026-02-09) Dark CSS variables are **fully defined** in globals.css under `.dark` class (lines 176-279). They invert the scale: 50=darkest, 900=lightest. - (2026-02-09) Root layout (`server/src/app/layout.tsx:116`) has `className={\`light\`}` hardcoded on ``. - (2026-02-09) Tailwind config has `background: 'white'` and `card: 'white'` hardcoded (lines 44, 63). These must become CSS variable references. - (2026-02-09) Status colors (success/warning/error) are hardcoded hex in Tailwind config (lines 51-62). Need CSS variable equivalents. - (2026-02-09) Radix UI `` is rendered without an `appearance` prop (layout.tsx:62). - (2026-02-09) `` has no theme configuration (layout.tsx:60). - (2026-02-09) globals.css has hardcoded `white` in switch thumb (lines 528, 580), `time-slot-working` (line 639), and `rgba(0,0,0,0.05)` in table hover (line 473) and `bg-dotted` (line 428). - (2026-02-09) Dark `.dark` block sets submenu to `--color-submenu-bg: #D0D5DD` (light gray) and `--color-submenu-text: #000000` (black) — this looks wrong for a dark theme and likely needs fixing. - (2026-02-09) 12 CSS module files contain hardcoded hex colors. Key files: `Dashboard.module.css`, `TextEditor.module.css`, `TicketDetails.module.css`, `ProjectDetail.module.css`, `billing-dashboard/*.module.css`. - (2026-02-09) Agent scan found ~5,084 Tailwind color class references across ~602 files. Zero `dark:` prefixed classes exist currently. - (2026-02-09) Zero hardcoded hex colors in TSX files — all color application is via Tailwind classes or CSS variables. This is very good for migration. - (2026-02-09) `suppressHydrationWarning` is already set on `` — good for dynamic class application. - (2026-02-09) Extension iframe bridge (`iframeBridge.ts`) sends `theme_tokens` during bootstrap but doesn't re-send on theme change. Will need a theme change listener. ## Commands / Runbooks ### Start the App for Visual Testing ```bash # Option A: Full Docker stack (Community Edition) docker compose -f docker-compose.base.yaml -f docker-compose.ce.yaml up # Option B: Full Docker stack (Enterprise Edition, includes extensions) docker compose -f docker-compose.base.yaml -f docker-compose.ee.yaml up # Option C: Local dev (faster iteration — requires DB + Redis running) docker compose -f docker-compose.base.yaml up -d db redis npm run build:shared # first time only npm run dev # http://localhost:3000 # or with Turbopack: npm run dev:turbo ``` ### Package Install ```bash npm install next-themes --workspace server ``` ### Quick Dark Mode Smoke Test (Before Toggle Exists) ```javascript // In browser console — force dark mode: document.body.className = 'dark'; // Revert to light: document.body.className = 'light'; // Check CSS variable values: getComputedStyle(document.body).getPropertyValue('--color-text-900'); ``` ### Find Files Needing Migration ```bash # Files with hardcoded bg-white / bg-gray-* / text-gray-* etc rg -l 'bg-white|bg-gray-|text-gray-|text-black|border-gray-' --glob '*.tsx' packages/ server/src/ # CSS modules with hex colors rg -l '#[0-9a-fA-F]{3,8}' --glob '*.module.css' packages/ server/src/ # Count all Tailwind color refs rg -c 'bg-(white|gray|red|green|blue|amber|black)|text-(gray|black|white)|border-gray' --glob '*.tsx' packages/ server/src/ | wc -l ``` ### Validate Contrast Ratios Use Chrome DevTools: Inspect element → Styles → click the color swatch → contrast ratio is shown. Or use the Lighthouse accessibility audit. ## Links / References - **ThemeContext**: `server/src/context/ThemeContext.tsx` - **Root layout**: `server/src/app/layout.tsx` - **Tailwind config**: `server/tailwind.config.ts` - **globals.css**: `server/src/app/globals.css` - **UI Kit tokens**: `packages/ui-kit/src/theme/tokens.css` - **UI Kit useTheme hook**: `packages/ui-kit/src/hooks/useTheme.ts` - **Iframe bridge**: `server/src/lib/extensions/ui/iframeBridge.ts` - **UI Kit Showcase PRD**: `docs/plans/2026-02-04-ui-kit-showcase-extension/PRD.md` - **CSS modules with hex colors**: - `packages/ui/src/components/dashboard/Dashboard.module.css` - `packages/ui/src/editor/TextEditor.module.css` - `packages/ui/src/editor/TicketDetails.module.css` - `packages/tickets/src/components/ticket/TicketDetails.module.css` - `packages/projects/src/components/ProjectDetail.module.css` - `packages/billing/src/components/billing-dashboard/*.module.css` ## Resolved Questions - [x] Install `next-themes` — yes, decided. Pros far outweigh the single dependency cost. - [x] Persist to DB — yes, use existing `user_preferences` table. No migration needed. - [x] Client portal — include in this effort. - [x] Dark submenu colors — they're wrong, fix them. - [x] Branding preview — add dark/light mode preview toggle to branding settings page. - [x] Branded shades — auto-adjust by generating inverted scale for dark mode (same algorithm, reversed step mapping). No separate tenant dark colors. - [x] Preview format — single toggle switch, not side-by-side. - [x] CSS selectors — **verified compatible**. `.dark`/`.light` are bare class selectors at globals.css:176/282. `next-themes` applies to `` by default. Bare selectors match regardless of element. CSS variables cascade. No changes needed. ## Open Questions (All resolved) ## User Preferences Integration ### Existing infrastructure (no changes needed): - **Table**: `user_preferences` — PK `(tenant, user_id, setting_name)` - **Model**: `server/src/lib/models/userPreferences.tsx` — `UserPreferences.upsert()`, `.get()`, `.getAllForUser()` - **Service**: `packages/users/src/services/UserService.ts` — `getUserPreferences()`, `updateUserPreferences()` - **API**: `PUT /api/v1/users/[id]/preferences` — body: `{ theme: "dark" }` - **Example**: locale preference already stored this way ### Client Portal Layout Structure ``` ClientPortalLayoutClient.tsx ├── AppSessionProvider ├── I18nWrapper ├── BrandingProvider └── [needs ThemeProvider here] └── ClientPortalLayout (has hardcoded bg-gray-100) ``` ### CSS Selector Verification - `globals.css:176` → `.dark {` — bare class selector, not element-bound - `globals.css:282` → `.light {` — bare class selector, not element-bound - `next-themes` default → applies class to `document.documentElement` (``) - Old `ThemeContext` → applied class to `document.body` - **Result**: bare selectors match either element. CSS variables on `` cascade to `` and all descendants. No migration needed. - **Bonus**: `next-themes` also sets `style="color-scheme: dark"` on ``, which affects browser-native scrollbars and form controls. ### Branding Shade Generation Algorithm **File**: `packages/tenancy/src/lib/generateBrandingStyles.ts` Current `generateColorShades(hexColor)`: - **Light shades (50-400)**: Lerp towards white. `component + (255 - component) * factor` - 50: factor=0.95, 100: 0.90, 200: 0.75, 300: 0.60, 400: 0.30 - **Base (500)**: Unchanged original color - **Dark shades (600-900)**: Multiply. `component * multiplier` - 600: 0.85, 700: 0.70, 800: 0.50, 900: 0.30 - Output format: `"r g b"` (space-separated, no commas) **Dark mode strategy**: Generate same shades, then swap output mapping: ``` Light step 50 → Dark step 900 (lightest shade becomes highest step) Light step 100 → Dark step 800 Light step 200 → Dark step 700 Light step 300 → Dark step 600 Light step 400 → kept similar (or minor adjustment) Light step 500 → kept as-is (base color unchanged) Light step 600 → Dark step 400 Light step 700 → Dark step 300 Light step 800 → Dark step 200 Light step 900 → Dark step 100 (darkest shade becomes lowest step) ``` **Injection approach**: `BrandingProvider` must emit two selector blocks: ```css :root { --color-primary-50: ; ... } /* existing */ .dark { --color-primary-50: !important; ... } /* new */ ``` ### Key client portal files: - `server/src/app/client-portal/layout.tsx` — server layout, fetches branding + locale - `server/src/app/client-portal/ClientPortalLayoutClient.tsx` — client wrapper with providers - `packages/client-portal/src/components/layout/ClientPortalLayout.tsx` — UI shell with nav bar - `packages/tenancy/src/components/providers/BrandingProvider.tsx` — injects branded CSS variables - `packages/tenancy/src/lib/generateBrandingStyles.ts` — generates branded CSS on server side - `server/src/app/auth/client-portal/signin/page.tsx` — auth page (needs ThemeProvider too)