Skip to content

feature/dark mode#331

Open
Hunta wants to merge 35 commits intocodex-team:mainfrom
Hunta:feature/dark-mode
Open

feature/dark mode#331
Hunta wants to merge 35 commits intocodex-team:mainfrom
Hunta:feature/dark-mode

Conversation

@Hunta
Copy link
Copy Markdown

@Hunta Hunta commented Apr 1, 2026

Dark Mode for CodeX Docs

Summary

Adds a complete dark mode feature to CodeX Docs with system preference detection, manual toggle, localStorage persistence, and WCAG 2.1 AA accessibility compliance. Includes comprehensive test coverage (139 Playwright e2e + 30 Mocha unit tests) across Chromium, Firefox, and WebKit.

Built against the feature specification in .github/specs/dark-mode/, implementing all functional requirements (FR-2.1 through FR-2.5) and non-functional requirements (NFR-3.1 through NFR-3.5) defined in Requirements.md.

What Changed

Theme System (Phase 1)

  • ThemeManager singleton module handles initialization, preference detection, persistence, and theme switching
  • CSS custom properties architecture: light defaults in vars.pcss, dark overrides via [data-theme="dark"] selector in new dark-mode.pcss
  • Synchronous initialization before other modules to prevent flash of unstyled content (FOUC)
  • @media (prefers-color-scheme: dark) fallback for JS-disabled users, with :not([data-theme="light"]) guard to respect explicit user choice

UI Components (Phase 2)

  • Sun/moon toggle button in the header with animated icon swap
  • All 11 component stylesheets audited and migrated to CSS variables — zero hardcoded colors remain (2.8 audit results)
  • Components covered: header, sidebar, navigator, page, writing, button, copy-button, auth, table-of-content, error, greeting

Color Palette

  • Tailwind zinc neutrals: #18181B background, #27272A surfaces, #71717A borders, #E4E4E7 primary text
  • All color pairs verified against WCAG 2.1 AA contrast thresholds (4.5:1 for text, 3:1 for UI boundaries)
  • 5 initial contrast failures found and fixed during accessibility testing

Database Migration

  • Replaced unmaintained nedb (last updated 2016) with @seald-io/nedb — the original package crashes on Node.js ≥ 24 due to removed util.isDate() / util.isRegExp() functions
  • Drop-in replacement, identical on-disk format, no data migration needed

Test Coverage

Suite Tests Scope
Theme toggle 6 Button visibility, click, keyboard (Enter/Space), ARIA labels
Persistence 14 Save/restore, reload, clear, invalid values, rapid toggles, format validation
System preference 4 prefers-color-scheme emulation, saved pref overrides system
Components 16 CSS variable values per theme, rendered body/header/text colors
No-FOUC 4 data-theme present on load, DOM consistent with localStorage
Accessibility 26 WCAG AA contrast ratios, keyboard nav, focus visibility, ARIA semantics
Performance 12 Toggle < 100ms, CLS < 0.05, no FOUC, CSS architecture validation
Browser compat 19 × 3 CSS vars, toggle, localStorage, system pref, colors, events (Chromium + Firefox + WebKit)
Unit (Mocha) 30 ThemeManager init, setTheme, getCurrentTheme, getSystemPreference, persistence, events

Total: 169 tests, all passing

Bugs Found & Fixed During Testing

Bug Root Cause Fix
Toggle icons never updated Wrong event name in listener Listen to themeChange
Page white in dark mode Hardcoded background: white on body/header/copy-button Replaced with var(--color-bg-main)
Greeting page lost dark mode Missing main.bundle.js in index.twig Added script tag
Alias 500 error on page save Missing await on alias.save() Added await in pages controller
Server crash on Node 24 nedb uses removed util.isDate() Migrated to @seald-io/nedb
applyTheme(null) on invalid localStorage hasSavedPreference() true but value invalid Fixed init() fallback chain
System dark overrides explicit light choice CSS @media :root ties with [data-theme="light"] Added :not([data-theme="light"]) guard

Files Changed (excluding lock files and spec docs)

  • 35 files changed, ~2,700 lines added, ~45 removed
  • New source files: themeManager.js, themeToggle.js, dark-mode.pcss, playwright.config.ts, 8 e2e spec files, 1 fixture, 1 unit test file
  • Modified source files: app.js, vars.pcss, header.pcss, sidebar.pcss, page.pcss, navigator.pcss, writing.pcss, copy-button.pcss, diff.pcss, main.pcss, header.twig, index.twig, local.ts, pages.ts, database.ts, package.json
  • Project docs: README.md (feature list), DEVELOPMENT.md (prerequisites, dark mode, testing, nedb migration)

Documentation

Full implementation documentation is in .github/specs/dark-mode/documentation/:

How to Test

# Start dev server
yarn dev

# Run all e2e tests (auto-starts server)
yarn test:e2e

# Run unit tests
yarn test

# Interactive Playwright UI
yarn test:e2e:ui

Toggle the sun/moon button in the header to switch themes. Preference persists across reloads and pages. Remove localStorage key codex-docs-theme to reset to system default.

Getting Started

Prerequisites

  • Node.js 20+ is required (eslint-plugin-jsdoc@62.9.0+ only supports Node 20+)
  • Docker (optional, for containerized deployment)

Local Development with Yarn

  1. Install dependencies:

    yarn install --ignore-engines

    (The --ignore-engines flag bypasses Node version warnings for some transitive dependencies)

  2. Start the development server:

    yarn dev

    This runs the backend on http://localhost:3000 and watches frontend assets.

  3. Access the app:
    Open http://localhost:3000 and use the sun/moon toggle button in the header (top-right) to switch themes.

Production with Docker

  1. Create a local config file:

    # Copy the default config
    cp docs-config.yaml docs-config.local.yaml

    Update port: 3000 and host: "0.0.0.0" in docs-config.local.yaml if needed.

  2. Build and start the container:

    docker compose up -d --build

    The app will be available at http://localhost:3000

  3. Verify the container:

    docker logs codexdocs-docs-1 --tail 20

    You should see: CodeX Docs server is running with Main page: http://localhost:3000

Important Notes

  • Node 22 on local machine: If you're running Node 22 locally and encounter eslint-plugin-jsdoc engine errors during yarn install, use the --ignore-engines flag:

    yarn install --ignore-engines
  • Docker builds: The Dockerfile.prod uses Node 20 and includes --ignore-engines flags in yarn install commands to ensure compatibility.

  • Database: This PR migrates from nedb to @seald-io/nedb. The new package is a drop-in replacement with identical on-disk format — no data migration required.

Hunta added 30 commits March 31, 2026 12:50
…ality

- Add theme toggle button to header.twig with sun/moon SVG icons
- Implement accessible button with aria-label and title attributes
- Create ThemeToggle module for click handling and icon updates
- Add theme-toggle button styles to header.pcss with CSS variables
- Implement hover, focus, and active states for accessibility
- Update app.js to initialize ThemeToggle module
- Button position: right-aligned in header menu via margin-left: auto
- Icon display toggles based on current theme automatically
- Click handler toggles between light/dark themes and persists to localStorage
- All styling uses CSS variables (--color-text-main, --color-link-hover)
- Project builds without errors: npm run build-frontend and build-backend SUCCESS
- Update Tasks.md to mark Tasks 2.1, 2.2, 2.3 as complete

Build Status:  VERIFIED
- Frontend: 8 assets, 230 modules, 1 pre-existing warning
- Backend: TypeScript compilation successful
…on and Header Toggle Button comprehensive guides
…ed colors with CSS variables and reorganize documentation
….md with build verification and completion dates
…coded colors with CSS variables and add dark mode support
… mode support with comprehensive documentation
…ion and verification

Complete CSS variable migration with 100% component coverage

- Audit all 11 component files for hardcoded colors
- Found and fixed 2 hardcoded colors in sidebar.pcss
- Replace gradient colors with CSS variables
- Replace focus state color with CSS variable
- Add 4 new CSS variables with light and dark mode values
- Add system preference fallback in @media (prefers-color-scheme: dark)
- Verify frontend and backend builds (0 errors, 230 modules)
- Create comprehensive task documentation (1340+ lines)
- Update Tasks.md with complete task 2.8 acceptance criteria

Result: 100% CSS variable coverage - all 11 components now fully theme-aware
The ThemeToggle module was instantiated but never initialized, causing the
theme toggle button to be non-functional in the UI. The module's init()
method is now called during document ready, ensuring the button properly
listens for clicks and updates the theme.

This was the final blocker preventing dark mode toggle from working in
the deployed application.
…s, and NeDB migration

Dark Mode Color Palette (neutral zinc grays):
- Redesigned dark-mode.pcss with Tailwind zinc-based neutral palette
  (bg: #18181B, surface: #27272A, borders: #3F3F46, text: #E4E4E7)
- Vibrant accent colors for syntax highlighting (indigo keywords, cyan
  variables, pink params, emerald classes)
- System preference fallback (@media prefers-color-scheme) mirrors the
  data-theme='dark' values exactly
- Added diff color CSS variables to vars.pcss (light) and dark-mode.pcss

Structural CSS Fixes:
- main.pcss: Added background: var(--color-bg-main) to body element
- header.pcss: Replaced hardcoded 'background: white' with CSS variable
- copy-button.pcss: Replaced hardcoded 'background: white' with CSS variable
- diff.pcss: Converted 4 hardcoded colors to CSS custom properties

Bug Fixes:
- themeToggle.js: Fixed event listener using wrong event name
  (was 'themeToggle' via onThemeToggle, now listens to 'themeChange'
  which is what ThemeManager.setTheme() actually dispatches)
- index.twig: Added main.bundle.js script tag so ThemeManager initializes
  on the greeting/landing page (dark mode was lost after login)
- pages.ts: Added missing 'await' on alias.save() in both insert() and
  update() methods, fixing race condition where page redirect arrived
  before alias was persisted to database

NeDB Migration (Node 24 compatibility):
- Replaced unmaintained 'nedb' 1.8.0 with '@seald-io/nedb' (maintained fork)
- Fixes util.isDate, util.isRegExp, util.isArray removal in Node 24
- Updated imports in local.ts and test/database.ts using createRequire()
  for clean CJS/ESM interop with Node16 module resolution
- Added explicit callback parameter types for @seald-io/nedb type defs

Playwright E2E Test Suite (35 tests):
- playwright.config.ts: Chromium-only config with webServer auto-start
- e2e/fixtures/setup.ts: Shared selectors, color constants, helpers
- theme-toggle.spec.ts: Button visibility, ARIA, click/keyboard toggle
- theme-persistence.spec.ts: localStorage save/restore across reloads
- system-preference.spec.ts: prefers-color-scheme emulation fallback
- components.spec.ts: CSS variable verification, rendered element colors
- no-fouc.spec.ts: Flash-of-unstyled-content prevention checks
- Added test scripts and @playwright/test dependency to package.json
- Updated .gitignore for Playwright artifacts
…y test suite

Audit all dark mode color pairs against WCAG 2.1 AA contrast requirements.
Fix 5 failures across both [data-theme=dark] and @media (prefers-color-scheme)
blocks, and add 26 new Playwright accessibility tests (61 total, all passing).

Contrast fixes (dark-mode.pcss):
- --color-line-gray: #3F3F46 -> #71717A (1.70:1 -> 3.67:1, WCAG 1.4.11 UI boundary)
- --color-code-comment: #71717A -> #909099 (3.84:1 -> 5.86:1, WCAG 1.4.3 text)
- --color-checkbox-border: #52525B -> #71717A (2.29:1 -> 3.67:1, WCAG 1.4.11 UI boundary)
- --color-button-primary: #3B82F6 -> #2563EB (3.68:1 -> 5.17:1, white text on bg)
  hover: #2563EB -> #1D4ED8, active: #1D4ED8 -> #1E40AF
- --color-button-warning: #FB923C -> #C2410C (2.26:1 -> 5.18:1, white text on bg)
  hover: #F97316 -> #9A3412, active: #EA580C -> #7C2D12

New test file (e2e/dark-mode/accessibility.spec.ts — 26 tests):
- 12 contrast ratio tests: live CSS variable extraction with luminance calculation
- 4 keyboard navigation tests: Tab reachability, Enter/Space activation
- 2 focus visibility tests (WCAG 2.4.7): toggle and link focus-visible indicators
- 8 ARIA/semantic structure tests: aria-label, semantic elements, SVG a11y, no duplicate IDs

Other changes:
- e2e/fixtures/setup.ts: update --color-line-gray expected value to match fix
- Rename 3.0-development-summary.md -> 3-progress-report.md, add Phase 3.2 details
…t, FOUC, CSS architecture

Add 12 Playwright performance tests verifying NFR-3.1.1 through NFR-3.1.3.
All 73 tests passing (61 existing + 12 new).

New test file (e2e/dark-mode/performance.spec.ts):

Theme Switch Timing (NFR-3.1.1 — < 100ms):
- Light-to-dark and dark-to-light toggle timing via performance.now()
- 10 rapid successive toggles each validated under 100ms
- Raw setAttribute performance baseline

No Layout Shift (NFR-3.1.2):
- PerformanceObserver CLS measurement during toggle (threshold: < 0.05)
- Header/sidebar dimensions stable across theme switch
- Scroll position preserved after toggle

No FOUC on Page Load:
- MutationObserver verifies first data-theme write is 'dark' (no light flash)
- Theme application completes within DOMContentLoaded

CSS Variables Architecture (NFR-3.1.3):
- Confirms data-theme attribute mechanism (not class swapping)
- CSS variables update synchronously with attribute change
- No inline color styles on layout elements
Add 9 new persistence tests to theme-persistence.spec.ts covering edge cases
from FR-2.2.1 through FR-2.2.4. All 82 tests passing.

New tests:
- Cross-page navigation: theme survives navigating away and back
- localStorage.clear(): resets to default light mode
- removeItem(key): resets to default light mode
- Invalid value ('invalid-theme'): no crash, toggle still functional
- Rapid toggles (7x): final state persisted correctly, survives reload
- Storage format: value is plain string, not JSON object
- No key leakage: only codex-docs-theme key in storage
- CSS variables: --color-bg-main and --color-text-main correct after reload
- Toggle icon: sun/moon visibility correct after reload
…ager invalid-value bugfix

Add 19 browser-compat tests run across Chromium, Firefox, and WebKit (57 total
cross-browser runs). Fix ThemeManager bug where invalid localStorage values
caused applyTheme(null). All 139 tests passing.

ThemeManager bugfix (themeManager.js):
- init() now handles invalid localStorage values: hasSavedPreference()
  returning true while getSavedPreference() returns null correctly falls
  back to system preference or light default

Playwright config (playwright.config.ts):
- Add chromium-compat, firefox-compat, webkit-compat projects
- browser-compat.spec.ts runs on all 3 browsers
- Existing chromium project excludes compat tests to avoid duplication

New test file (e2e/dark-mode/browser-compat.spec.ts — 19 tests x 3 browsers):
- CSS custom properties: light/dark resolution, [data-theme] selector override
- Theme toggle: click both directions, icon swap, keyboard Enter/Space
- localStorage: save, restore after reload, API availability check
- System preference: matchMedia API, prefers-color-scheme dark/light emulation
- Rendered colors: body bg dark/light, text color, header bg
- API support: CustomEvent dispatch, MutationObserver attribute detection

Updated tests (theme-persistence.spec.ts):
- Invalid value test updated: now asserts fallback to light mode
Remove dead code from ThemeManager and fix CSS specificity bug.
All 139 tests passing after changes.

ThemeManager cleanup (themeManager.js — 227 → 173 lines):
- Remove unused onThemeToggle(callback): no code references this method
- Remove unused static toggleTheme(): ThemeToggle calls setTheme() directly

CSS specificity bugfix (dark-mode.pcss):
- @media (prefers-color-scheme: dark) selector changed from :root to
  :root:not([data-theme='light']) to prevent system dark preference from
  overriding an explicit user choice of light mode when JS has set the
  attribute. JS-disabled users still get dark mode correctly.

Documentation:
- Rename 3-progress-report.md → progress-report.md
- Add Phase 4.1 completion details
Relocate Playwright e2e test suite from top-level e2e/ into src/test/e2e/
to align with existing test directory structure. Update testDir in
playwright.config.ts accordingly.
Add 30 Mocha unit tests for ThemeManager covering all public methods and
edge cases using JSDOM for browser environment simulation.

New file (src/test/modules/themeManager.ts — 30 tests):
- init(): saved preference, system preference, default fallback, idempotent
- getCurrentTheme(): returns current theme, reads from DOM if not set
- setTheme(): applies theme and persists, emits themeChange event,
  rejects invalid values, handles localStorage quota errors
- applyTheme(): sets data-theme attribute, updates internal state
- getSystemPreference(): detects dark/light, caches result, handles errors
- hasSavedPreference(): checks localStorage key existence, handles errors
- getSavedPreference(): reads valid values, rejects invalid, handles errors
- emitThemeChange(): dispatches CustomEvent with theme detail
- Invalid value fallback: init() with invalid stored value falls back correctly

Dependencies:
- jsdom (devDependency) for browser environment simulation

Run with: npx ts-mocha src/test/modules/themeManager.ts --timeout 5000
Remove 12 documentation files that became inaccurate after Phase 3-4 changes
(removed ThemeManager methods, color palette redesign, WCAG fixes, test moves).

Deleted (outdated — referenced removed API methods, old color palette):
- CHECKPOINT_REPORT.md, COMPLETION_SUMMARY.md, SESSION_COMPLETION.md
- 1.1-theme-manager-foundation/ (3 files) — referenced onThemeToggle(), toggleTheme()
- 1.2-css-variables-infrastructure/ (3 files) — old VS Code palette colors
- 2.1-2.3-header-toggle-button/ (3 files) — referenced onThemeToggle()

Kept (verified accurate):
- 1.3-app-initialization/, 2.4-2.8 component docs — still correct
- progress-report.md — authoritative doc (paths fixed: e2e/ → src/test/e2e/)
- Requirements.md, Design.md, Tasks.md, README.md — spec docs
- ImplementationSummary.md: complete test suite overview, bugs found/fixed, WCAG fixes, palette redesign, database migration
- QuickReference.md: test commands, file map, fixture helpers, color reference, WCAG thresholds, troubleshooting
- TechnicalDeepDive.md: architecture, multi-browser config, WCAG calculation methodology, JSDOM setup, CSS specificity analysis, performance measurement, root cause analyses
…ENT.md)

- README.md: Add dark mode to Features list
- DEVELOPMENT.md: Add Prerequisites section with Node.js >= 18 requirement
- DEVELOPMENT.md: Document nedb -> @seald-io/nedb migration (Node 24 compat)
- DEVELOPMENT.md: Add Dark Mode section and Testing section
- progress-report.md: Mark Phase 4.4 as done
@Hunta Hunta mentioned this pull request Apr 1, 2026
@Hunta Hunta force-pushed the feature/dark-mode branch from a39147f to 1e81fd9 Compare April 1, 2026 07:35
- Update Dockerfile.prod to use Node 20 with --ignore-engines for yarn installs
- Update docker-compose.yml to build from local source with proper port mapping
- Add docs-config.local.yaml with required port and configuration
- Update PULL_REQUEST.md with Getting Started guide for yarn dev and Docker deployment
- Document Node.js version requirements and --ignore-engines usage
- Include database migration notes for nedb to @seald-io/nedb"
@Hunta Hunta force-pushed the feature/dark-mode branch from 5b72557 to 8c1b063 Compare April 3, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant