Skip to content

fix(theme): remove stale dark root class in light mode#1960

Merged
elibosley merged 1 commit intomainfrom
codex/fix-light-theme-dark-root
Mar 24, 2026
Merged

fix(theme): remove stale dark root class in light mode#1960
elibosley merged 1 commit intomainfrom
codex/fix-light-theme-dark-root

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Mar 24, 2026

Summary

  • fix theme-store bootstrap so the DOM theme name is authoritative on first load
  • remove stale root/body dark classes when the active theme is light
  • add a regression test covering light theme startup with stale dark classes present

Root cause

The frontend bootstrap path could trust a pre-existing .dark class before reconciling against the authoritative DOM theme name. If that class lingered on <html> from an earlier client-side mutation, a light theme could still boot into Tailwind dark mode.

Validation

  • pnpm test __test__/store/theme.test.ts
  • pnpm test __test__/components/ThemeSwitcher.test.ts

Reference

Summary by CodeRabbit

  • Tests

    • Added test coverage for theme initialization to verify correct handling of light theme CSS variables and removal of stale dark mode classes.
  • Refactor

    • Improved theme store initialization logic to read theme settings from DOM CSS variables, with fallback to legacy initialization when needed.

- Purpose: ensure light themes do not inherit a stale `dark` class on initial page load.\n- Before: theme bootstrap trusted any existing root/body `dark` class before reconciling against the authoritative DOM theme name.\n- Problem: a stale client-side class could survive into a light-theme render and keep Tailwind dark styles active.\n- Change: make `--theme-name` authoritative during store initialization and only fall back to class-based dark-mode bootstrap when no theme name is present.\n- Coverage: add a regression test that starts with a light DOM theme plus stale `dark` classes and verifies they are removed.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 24, 2026

Walkthrough

This pull request refactors theme store initialization to read DOM-based theme configuration via CSS variables instead of computing dark mode status directly. A new helper function isDarkThemeName() centralizes dark-theme detection logic, and a test case verifies proper class removal behavior when initializing with a light theme.

Changes

Cohort / File(s) Summary
Test Coverage
web/__test__/store/theme.test.ts
Added test case verifying that when DOM --theme-name is set to white, the store removes stale 'dark' classes from document.documentElement.classList and document.body.classList, confirming darkMode === false.
Theme Store Implementation
web/src/store/theme.ts
Introduced isDarkThemeName() helper function to consolidate dark-theme applicability logic. Refactored store initialization to prioritize DOM-based theme reading via readDomThemeName() over direct isDarkModeActive() calls, with fallback to previous behavior when DOM theme is absent.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A theme so bright, no dark remains,
DOM variables guide through logic's lanes,
Helper functions clean the code's bright gate,
Dark classes fade to their rightful fate!
wiggles nose

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(theme): remove stale dark root class in light mode' directly and specifically describes the main change: removing stale dark CSS classes when a light theme is active on initialization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-light-theme-dark-root

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Mar 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.95%. Comparing base (0e004a7) to head (70d5b38).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1960      +/-   ##
==========================================
+ Coverage   51.74%   51.95%   +0.21%     
==========================================
  Files        1028     1030       +2     
  Lines       70792    71122     +330     
  Branches     7881     7933      +52     
==========================================
+ Hits        36630    36951     +321     
- Misses      34039    34048       +9     
  Partials      123      123              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1960/dynamix.unraid.net.plg

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
web/src/store/theme.ts (1)

51-53: Normalize the theme name here and drop the cast.

mapPublicTheme() already lowercases server values, but this helper still compares raw strings and needs a type assertion to compile. If --theme-name ever arrives as Black/Gray, the first-load bootstrap will miss the dark path. Normalizing once here lets you keep the check typed without the cast.

♻️ Suggested change
-const isDarkThemeName = (themeName: string) =>
-  DARK_UI_THEMES.includes(themeName as (typeof DARK_UI_THEMES)[number]);
+const isDarkThemeName = (themeName: string) => {
+  const normalizedThemeName = themeName.trim().toLowerCase();
+  return DARK_UI_THEMES.some((darkThemeName) => darkThemeName === normalizedThemeName);
+};

As per coding guidelines Avoid using casting whenever possible, prefer proper typing from the start.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/store/theme.ts` around lines 51 - 53, The isDarkThemeName helper
compares the raw themeName and uses a cast to satisfy types, which breaks when
theme names vary in case; normalize themeName (e.g., toLowerCase) inside
isDarkThemeName before checking DARK_UI_THEMES so the comparison is
case-insensitive and you can remove the type assertion; ensure this matches the
normalization done by mapPublicTheme and handles values coming from --theme-name
like "Black"/"Gray" so first-load bootstrapping correctly detects dark themes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/__test__/store/theme.test.ts`:
- Around line 290-300: The test "should remove stale dark classes when the DOM
theme is light" is asserting mocked classList.remove calls (which are stubbed in
beforeEach) instead of verifying the actual DOM state; update the test to
restore or avoid stubbing document.documentElement.classList.remove and
document.body.classList.remove for this case (or temporarily replace them with
the real methods) before calling createStore(), then assert that
document.documentElement.classList.contains('dark') and
document.body.classList.contains('dark') are false and that store.darkMode is
false; reference the existing test block and the createStore() invocation to
locate where to adjust the stubbing/restore.

---

Nitpick comments:
In `@web/src/store/theme.ts`:
- Around line 51-53: The isDarkThemeName helper compares the raw themeName and
uses a cast to satisfy types, which breaks when theme names vary in case;
normalize themeName (e.g., toLowerCase) inside isDarkThemeName before checking
DARK_UI_THEMES so the comparison is case-insensitive and you can remove the type
assertion; ensure this matches the normalization done by mapPublicTheme and
handles values coming from --theme-name like "Black"/"Gray" so first-load
bootstrapping correctly detects dark themes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 39e9ac9f-168d-4c16-9f72-2c88cde32290

📥 Commits

Reviewing files that changed from the base of the PR and between ea41225 and 70d5b38.

📒 Files selected for processing (2)
  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts

Comment on lines +290 to +300
it('should remove stale dark classes when the DOM theme is light', () => {
document.documentElement.style.setProperty('--theme-name', 'white');
originalDocumentElementAddClass.call(document.documentElement.classList, 'dark');
originalAddClassFn.call(document.body.classList, 'dark');

const store = createStore();

expect(document.documentElement.classList.remove).toHaveBeenCalledWith('dark');
expect(document.body.classList.remove).toHaveBeenCalledWith('dark');
expect(store.darkMode).toBe(false);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert the final DOM state instead of the mocked remove() calls.

In this file, classList.remove is stubbed to vi.fn() in beforeEach, so these expectations can pass while dark is still present on <html> and <body>. Restoring the real remove methods for this case (or not stubbing them) and asserting contains('dark') === false will make this a real regression test instead of an implementation-detail check.

💡 Suggested change
   it('should remove stale dark classes when the DOM theme is light', () => {
+    document.documentElement.classList.remove = originalDocumentElementRemoveClass;
+    document.body.classList.remove = originalRemoveClassFn;
     document.documentElement.style.setProperty('--theme-name', 'white');
     originalDocumentElementAddClass.call(document.documentElement.classList, 'dark');
     originalAddClassFn.call(document.body.classList, 'dark');
 
     const store = createStore();
 
-    expect(document.documentElement.classList.remove).toHaveBeenCalledWith('dark');
-    expect(document.body.classList.remove).toHaveBeenCalledWith('dark');
+    expect(document.documentElement.classList.contains('dark')).toBe(false);
+    expect(document.body.classList.contains('dark')).toBe(false);
     expect(store.darkMode).toBe(false);
   });

As per coding guidelines Test what the code does, not implementation details like exact error message wording.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/store/theme.test.ts` around lines 290 - 300, The test "should
remove stale dark classes when the DOM theme is light" is asserting mocked
classList.remove calls (which are stubbed in beforeEach) instead of verifying
the actual DOM state; update the test to restore or avoid stubbing
document.documentElement.classList.remove and document.body.classList.remove for
this case (or temporarily replace them with the real methods) before calling
createStore(), then assert that
document.documentElement.classList.contains('dark') and
document.body.classList.contains('dark') are false and that store.darkMode is
false; reference the existing test block and the createStore() invocation to
locate where to adjust the stubbing/restore.

@elibosley elibosley merged commit 2815f85 into main Mar 24, 2026
13 checks passed
@elibosley elibosley deleted the codex/fix-light-theme-dark-root branch March 24, 2026 20:19
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