|
| 1 | +# Theme System - Work in Progress |
| 2 | + |
| 3 | +## Current Status |
| 4 | + |
| 5 | +### What's Working |
| 6 | +- ✅ Theme picker tests: 9/10 @ready (63 tests passing across 7 browsers) |
| 7 | +- ✅ Manual theme selection and persistence works correctly |
| 8 | +- ✅ Theme switching via theme picker UI works |
| 9 | +- ✅ View Transitions correctly maintain theme across page navigations |
| 10 | +- ✅ CSS architecture refactored: `@theme inline` uses `var()` references instead of hard-coded values |
| 11 | + |
| 12 | +### What's Broken |
| 13 | +- ❌ **System preference (prefers-color-scheme) not respected on first visit in real browser** |
| 14 | + - Test passes but real usage fails |
| 15 | + - Opening site in new incognito window with dark mode preference shows light theme |
| 16 | + - Google.com correctly shows dark, but our site doesn't |
| 17 | + |
| 18 | +- ❌ **Theme picker buttons should show their own theme's colors** |
| 19 | + - Dark theme button should use `--dark-color-*` variables |
| 20 | + - Default theme button should use `--light-color-*` variables |
| 21 | + - Currently all buttons use light theme colors (partially fixed but needs completion) |
| 22 | + |
| 23 | +## Root Cause Analysis |
| 24 | + |
| 25 | +### System Preference Issue |
| 26 | + |
| 27 | +The problem is a **race condition** between: |
| 28 | +1. HEAD inline script (synchronous) - correctly sets `data-theme="dark"` based on `prefers-color-scheme` |
| 29 | +2. `persistentAtom` restore (asynchronous) - overwrites theme back to 'default' |
| 30 | + |
| 31 | +**Sequence of events:** |
| 32 | +``` |
| 33 | +1. HEAD script runs → checks localStorage (empty) → checks prefersDark=true → sets data-theme="dark" ✓ |
| 34 | +2. persistentAtom initializes with default value 'default' |
| 35 | +3. Our init code runs with setTimeout(100ms) |
| 36 | +4. persistentAtom's restore() completes (async) → fires .listen() → overwrites to 'default' ✗ |
| 37 | +``` |
| 38 | + |
| 39 | +**Current fix attempt:** |
| 40 | +- Using `isInitialized` flag to prevent `.listen()` from applying themes until init completes |
| 41 | +- Using `setTimeout(100ms)` to delay init until after `persistentAtom.restore()` completes |
| 42 | +- **Problem:** The timing is unreliable - 100ms might not be enough on slower devices |
| 43 | + |
| 44 | +**Test vs Reality:** |
| 45 | +- Playwright test passes because it's using `emulateMedia({ colorScheme: 'dark' })` |
| 46 | +- Real browser behavior is different - the race condition manifests differently |
| 47 | +- Test needs to be improved to catch this real-world bug |
| 48 | + |
| 49 | +## Files Modified |
| 50 | + |
| 51 | +### Theme Initialization |
| 52 | +- `src/components/Scripts/state/store/themes.ts` (lines 127-180) |
| 53 | + - Changed from `.subscribe()` to `.listen()` to avoid immediate firing |
| 54 | + - Added `isInitialized` flag to gate theme applications |
| 55 | + - Added `setTimeout(100)` to wait for `persistentAtom.restore()` |
| 56 | + - **HAS DEBUG LOGGING** - needs to be removed before commit |
| 57 | + |
| 58 | +### Theme Picker UI |
| 59 | +- `src/components/ThemePicker/Themes.astro` (lines 97-120) |
| 60 | + - **NOT YET FIXED** - still needs to map theme.id to color prefix |
| 61 | + - Should use `--${colorPrefix}-color-*` variables per button |
| 62 | + |
| 63 | +### HEAD Script |
| 64 | +- `src/components/Head/index.astro` (lines 56-63) |
| 65 | + - Correctly checks `prefers-color-scheme` and sets `data-theme` |
| 66 | + - Logic: stored theme (if not 'default') > system preference > 'default' |
| 67 | + |
| 68 | +### CSS Architecture |
| 69 | +- `src/styles/themes.css` |
| 70 | + - ✅ Lines 65-105: `@theme inline` refactored to use `var()` references |
| 71 | + - ✅ All theme-specific colors defined at `:root` level |
| 72 | + - Has `--light-color-*`, `--dark-color-*`, and base `--color-*` variables |
| 73 | + |
| 74 | +## Next Steps |
| 75 | + |
| 76 | +### High Priority |
| 77 | +1. **Fix system preference detection** |
| 78 | + - Option A: Find more reliable way to detect when `persistentAtom.restore()` completes |
| 79 | + - Option B: Use `MutationObserver` to watch for theme changes from restore |
| 80 | + - Option C: Initialize theme BEFORE importing `persistentAtom` |
| 81 | + - Option D: Use regular `atom` for store, manually sync to localStorage after restore completes |
| 82 | + - Option E: Don't rely on setTimeout - use `requestIdleCallback` or similar |
| 83 | + |
| 84 | +2. **Fix theme picker button colors** |
| 85 | + - Complete the Themes.astro fix to show each theme's own colors |
| 86 | + - Map `theme.id` to color variable prefix: 'default' → 'light', 'dark' → 'dark' |
| 87 | + - Update color swatches to use theme-specific variables |
| 88 | + |
| 89 | +3. **Remove debug logging** |
| 90 | + - `src/components/Scripts/state/store/themes.ts` has console.log statements |
| 91 | + - Clean these up before final commit |
| 92 | + |
| 93 | +### Test Improvements |
| 94 | +4. **Make test match real browser behavior** |
| 95 | + - Current test uses `emulateMedia()` which might not trigger same race condition |
| 96 | + - Consider testing with actual localStorage clearing and page reload |
| 97 | + - Add test that validates theme immediately on page load (before JS runs) |
| 98 | + |
| 99 | +## Technical Constraints |
| 100 | + |
| 101 | +- **MUST use `persistentAtom`** - required for View Transitions to maintain theme across navigations |
| 102 | +- **CANNOT use regular `atom`** - will lose persistence across page navigations |
| 103 | +- HEAD script must run synchronously to prevent FOUC (Flash of Unstyled Content) |
| 104 | +- Theme must be applied before page renders (critical for UX) |
| 105 | + |
| 106 | +## Code Locations |
| 107 | + |
| 108 | +- Theme store: `src/components/Scripts/state/store/themes.ts` |
| 109 | +- Theme picker UI: `src/components/ThemePicker/Themes.astro` |
| 110 | +- Theme picker element: `src/components/ThemePicker/theme-picker-element.ts` |
| 111 | +- HEAD script: `src/components/Head/index.astro` (lines 56-63) |
| 112 | +- CSS themes: `src/styles/themes.css` |
| 113 | +- E2E tests: `test/e2e/specs/04-components/theme-picker.spec.ts` (line 134 is failing test) |
| 114 | + |
| 115 | +## Questions to Answer |
| 116 | + |
| 117 | +1. When exactly does `persistentAtom.restore()` complete? |
| 118 | +2. Is there an event or promise we can wait for? |
| 119 | +3. Should we implement our own localStorage persistence instead of using `persistentAtom`? |
| 120 | +4. Can we leverage the `@media (prefers-color-scheme: dark)` CSS to avoid needing JS for system preference? |
| 121 | + |
| 122 | +## Useful Context |
| 123 | + |
| 124 | +- The `@media (prefers-color-scheme: dark)` CSS rule at lines 337-373 in themes.css correctly applies dark theme |
| 125 | +- This CSS works WITHOUT JavaScript |
| 126 | +- The issue is the JS is overriding this CSS by setting `data-theme="default"` |
| 127 | +- Maybe we should NOT set `data-theme` at all when using system preference? |
0 commit comments