Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
15 KiB
15 KiB
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
<body>(.light/.dark) — this matches the existing globals.css structure and is compatible with Tailwind'sdarkMode: 'class'. - (2026-02-09) The ui-kit uses
data-theme="dark"on<html>— 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 brokenThemeContext. Hydration mismatch handled withmountedguard on toggle UI. - (2026-02-09) PERSIST TO DB — store theme preference in existing
user_preferencestable (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
ThemeProvidertoClientPortalLayoutClient.tsx, add toggle to nav bar, fixbg-gray-100hardcoding, ensureBrandingProviderworks in dark mode, cover auth pages. - (2026-02-09) FIX DARK SUBMENU COLORS — current
.darkvalues (--color-submenu-bg: #D0D5DD,--color-submenu-text: #000000) are incorrect for dark mode. Fix to proper dark palette (e.g.,#1f2937bg,#f5f5f5text). - (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 —
BrandingProviderwill 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/.lightare bare class selectors (notbody.darkorhtml.dark). They match<html class="dark">(next-themes default target) correctly. CSS variables cascade to all descendants. No selector changes needed. - (2026-02-10) Installed
next-themesinserverworkspace via npm workspaces to satisfy FR-1.1. - (2026-02-10) Enabled Tailwind dark mode via
darkMode: 'class'inserver/tailwind.config.ts. - (2026-02-10) Added theme-aware
--color-background/--color-cardCSS variables and switched Tailwindbackground/cardcolors to use them. - (2026-02-10) Added theme-aware status color CSS variables and wired Tailwind
success/warning/errorcolors to them. - (2026-02-10) Replaced the light-locked ThemeContext with
next-themesviaAppThemeProviderin the root layout and removed the old context file. - (2026-02-10) Added
useAppTheme()hook to load theme preference from/api/v1/users/:id/preferencesand persist changes back to the DB. - (2026-02-10) Removed hardcoded
className="light"from root<body>so next-themes controls the theme class. - (2026-02-10) Added
ThemeBridgeto map resolved theme to Radixappearanceand MantineforceColorScheme. - (2026-02-10) Created
ThemeToggledropdown (Light/Dark/System) powered byuseAppTheme. - (2026-02-10) Added
ThemeToggleto the MSP header actions area. - (2026-02-10) Moved
ThemeToggleanduseAppThemeinto@alga-psa/uiso 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_preferencesvia the v1 preferences API. - (2026-02-10) useAppTheme loads the saved theme from
user_preferenceson auth and applies it. - (2026-02-10) Added CSS variable mapping to sync
--color-*tokens to--alga-*tokens and setdata-themeviaThemeBridgeso 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-thumband added checkbox/radio accent colors for dark mode. - (2026-02-10) Updated
time-slot-workingbackground 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-backgroundinstead of hardcoded gray. - (2026-02-10) Added inverted branding shade generation and
.darkscoped 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
.darkclass.
Discoveries / Constraints
- (2026-02-09)
ThemeContext(server/src/context/ThemeContext.tsx) is completely hardlocked to light —setThemeStatusignores its argument and always setslight. This was intentional (comment says "Always set to light mode"). - (2026-02-09) Dark CSS variables are fully defined in globals.css under
.darkclass (lines 176-279). They invert the scale: 50=darkest, 900=lightest. - (2026-02-09) Root layout (
server/src/app/layout.tsx:116) hasclassName={\light`}hardcoded on`. - (2026-02-09) Tailwind config has
background: 'white'andcard: '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
<Theme>is rendered without anappearanceprop (layout.tsx:62). - (2026-02-09)
<MantineProvider>has no theme configuration (layout.tsx:60). - (2026-02-09) globals.css has hardcoded
whitein switch thumb (lines 528, 580),time-slot-working(line 639), andrgba(0,0,0,0.05)in table hover (line 473) andbg-dotted(line 428). - (2026-02-09) Dark
.darkblock 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)
suppressHydrationWarningis already set on<body>— good for dynamic class application. - (2026-02-09) Extension iframe bridge (
iframeBridge.ts) sendstheme_tokensduring bootstrap but doesn't re-send on theme change. Will need a theme change listener.
Commands / Runbooks
Start the App for Visual Testing
# 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
npm install next-themes --workspace server
Quick Dark Mode Smoke Test (Before Toggle Exists)
// 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
# 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.csspackages/ui/src/editor/TextEditor.module.csspackages/ui/src/editor/TicketDetails.module.csspackages/tickets/src/components/ticket/TicketDetails.module.csspackages/projects/src/components/ProjectDetail.module.csspackages/billing/src/components/billing-dashboard/*.module.css
Resolved Questions
- Install
next-themes— yes, decided. Pros far outweigh the single dependency cost. - Persist to DB — yes, use existing
user_preferencestable. No migration needed. - Client portal — include in this effort.
- Dark submenu colors — they're wrong, fix them.
- Branding preview — add dark/light mode preview toggle to branding settings page.
- Branded shades — auto-adjust by generating inverted scale for dark mode (same algorithm, reversed step mapping). No separate tenant dark colors.
- Preview format — single toggle switch, not side-by-side.
- CSS selectors — verified compatible.
.dark/.lightare bare class selectors at globals.css:176/282.next-themesapplies to<html>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-boundglobals.css:282→.light {— bare class selector, not element-boundnext-themesdefault → applies class todocument.documentElement(<html>)- Old
ThemeContext→ applied class todocument.body - Result: bare selectors match either element. CSS variables on
<html>cascade to<body>and all descendants. No migration needed. - Bonus:
next-themesalso setsstyle="color-scheme: dark"on<html>, 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:
:root { --color-primary-50: <light-50>; ... } /* existing */
.dark { --color-primary-50: <inverted-900> !important; ... } /* new */
Key client portal files:
server/src/app/client-portal/layout.tsx— server layout, fetches branding + localeserver/src/app/client-portal/ClientPortalLayoutClient.tsx— client wrapper with providerspackages/client-portal/src/components/layout/ClientPortalLayout.tsx— UI shell with nav barpackages/tenancy/src/components/providers/BrandingProvider.tsx— injects branded CSS variablespackages/tenancy/src/lib/generateBrandingStyles.ts— generates branded CSS on server sideserver/src/app/auth/client-portal/signin/page.tsx— auth page (needs ThemeProvider too)