Hermes 284313f908
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
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

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's darkMode: '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 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 SHADESBrandingProvider 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 <html class="dark"> (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 <body> 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 <Theme> is rendered without an appearance prop (layout.tsx:62).
  • (2026-02-09) <MantineProvider> 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 <body> — 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

# 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.

  • 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

  • Install next-themes — yes, decided. Pros far outweigh the single dependency cost.
  • Persist to DB — yes, use existing user_preferences table. 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/.light are bare class selectors at globals.css:176/282. next-themes applies 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.tsxUserPreferences.upsert(), .get(), .getAllForUser()
  • Service: packages/users/src/services/UserService.tsgetUserPreferences(), 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 (<html>)
  • Old ThemeContext → applied class to document.body
  • Result: bare selectors match either element. CSS variables on <html> cascade to <body> and all descendants. No migration needed.
  • Bonus: next-themes also sets style="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 + 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)