Skip to content

feat(theme): introduce a mechanism to toggle light/dark modes#230

Merged
desmondinho merged 5 commits intodevfrom
feat/dark-mode-toggle
Jul 23, 2025
Merged

feat(theme): introduce a mechanism to toggle light/dark modes#230
desmondinho merged 5 commits intodevfrom
feat/dark-mode-toggle

Conversation

@desmondinho
Copy link
Contributor

@desmondinho desmondinho commented Jul 23, 2025

Description

Closes #169

This PR introduces a new mechanism for toggling light/dark modes.

What's been done?

  • {Change 1}
  • {Change 2}
  • ...

Checklist

  • My code follows the project's coding style and guidelines.
  • I have included inline docs for my changes, where applicable.
  • I have added, updated or removed tests according to my changes.
  • All tests are passing.
  • There's an open issue for the PR that I am making.

Additional Notes

Summary by CodeRabbit

  • New Features

    • Introduced dark mode support, including a dark mode toggle UI and documentation on enabling and switching themes.
    • Added a ThemeService for managing theme settings and persistence.
    • Added new documentation pages and interactive samples for theming.
  • Enhancements

    • Navigation now displays updated and new status badges with improved styling.
    • Improved consistency in status indicators across navigation and badges.
  • Bug Fixes

    • Corrected theme initialization and switching logic for better reliability.
  • Documentation

    • Expanded theming documentation, including guides and code samples for dark mode and theme toggling.
  • Tests

    • Added unit tests for theme management functionality.

@desmondinho desmondinho linked an issue Jul 23, 2025 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Jul 23, 2025

Walkthrough

This update introduces a comprehensive theming system with support for light, dark, and system themes. It adds a ThemeService for theme management, a LumexThemeProvider component, and corresponding JavaScript utilities. Navigation and badge components are updated to use a revised status enum. New documentation and sample files demonstrate dark mode features, and related tests are included.

Changes

File(s) Change Summary
docs/LumexUI.Docs.Client/Common/Enums/PageStatus.cs Renamed ComponentStatus to PageStatus; added Updated member.
docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs Updated navigation items to use PageStatus and added new statuses.
docs/LumexUI.Docs.Client/Common/Navigation/Types.cs Changed NavigationItem.Status type from ComponentStatus? to PageStatus?.
docs/LumexUI.Docs.Client/Components/NavItemBadge.razor, docs/LumexUI.Docs.Client/Components/NavMenu.razor Updated status parameter/type to PageStatus?; adjusted badge styling and logic for new status.
docs/LumexUI.Docs.Client/Components/Layouts/MainLayout.razor Removed <LumexThemeProvider /> from layout.
docs/LumexUI.Docs.Client/Pages/Theming/Customization/Customization.razor Replaced <ThemeTogglePreview /> with <CustomThemesPreview />.
docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/DarkMode.razor Added new documentation page for dark mode with setup and usage instructions.
docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/ThemeToggle.razor Added new interactive theme toggle component.
docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/ThemeTogglePreview.razor Added new preview component for the theme toggle UI.
docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeProvider.html Added sample for using LumexThemeProvider interactively.
docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeRoot.html Added sample HTML applying global dark mode.
docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeToggle.html Added sample for a dark mode toggle with dropdown menu.
docs/LumexUI.Docs.Client/_Imports.razor Added using LumexUI.Services.
docs/LumexUI.Docs/Components/App.razor Added script to set initial theme based on user/system preference.
docs/LumexUI.Docs/Components/Routes.razor Added <LumexThemeProvider> before the router.
src/LumexUI/Common/Enums/Theme.cs Added new Theme enum with Light, Dark, System.
src/LumexUI/Components/Providers/LumexThemeProvider.cs Added new LumexThemeProvider component implementing theming support.
src/LumexUI/Components/Providers/LumexThemeProvider.razor.cs Deleted empty partial class for LumexThemeProvider.
src/LumexUI/Extensions/ServiceCollectionExtensions.cs Registered ThemeService in DI; added private extension method for registration.
src/LumexUI/Services/Theme/ThemeService.cs Added new ThemeService for theme state management and JS interop.
src/LumexUI/wwwroot/js/components/popover.bundle.js Improved popover outside click handling, middleware ordering, and error reporting.
src/LumexUI/wwwroot/js/utils/theme.js Added new JS module for theme management (initialize, get, set, toggle, apply).
tests/LumexUI.Tests/Services/ThemeServiceTests.cs Added unit tests for ThemeService covering initialization, theme changes, toggling, and disposal.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ThemeToggle (Razor)
    participant ThemeService (.NET)
    participant JSThemeModule (JS)
    participant Browser

    User->>ThemeToggle: Clicks toggle button
    ThemeToggle->>ThemeService: SetThemeAsync(theme) / ToggleThemeAsync()
    ThemeService->>JSThemeModule: theme.set(theme) / theme.toggle()
    JSThemeModule->>Browser: Apply theme class to <html>
    JSThemeModule-->>ThemeService: Return new theme
    ThemeService-->>ThemeToggle: Update IsDarkMode
    ThemeToggle-->>User: UI updates to reflect new theme
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Suggested labels

📦 Scope: Components, 🚀 Type: Feature

Poem

A bunny hopped through code and night,
Bringing themes both dark and light.
With toggles, docs, and badges new,
The UI shines in every hue.
Now users choose the mode they crave—
Hooray for themes the rabbit gave!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 3

🧹 Nitpick comments (9)
docs/LumexUI.Docs.Client/Components/NavItemBadge.razor (1)

19-19: Consider using consistent sizing units.

The font size was changed from "text-[0.5rem]" to "text-[9px]". While both produce similar results (0.5rem ≈ 8px at default font size), mixing rem and pixel units can affect consistency across the design system.

Consider keeping rem-based sizing for better scalability:

-        .Add( "text-[9px]" )
+        .Add( "text-[0.5rem]" )
docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeToggle.html (1)

29-29: Consider using dynamic ID generation for reusability.

The fixed dropdown ID "theme-toggle" could cause conflicts if multiple theme toggle components are used on the same page.

Consider generating a unique ID:

-    private string _dropdownId = "theme-toggle";
+    private string _dropdownId = $"theme-toggle-{Guid.NewGuid():N}";

Or use a more lightweight approach:

-    private string _dropdownId = "theme-toggle";
+    private string _dropdownId = $"theme-toggle-{GetHashCode()}";
docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/ThemeToggle.razor (2)

4-10: Consider adding accessibility attributes.

The button implementation is solid with proper icon switching logic. Consider adding an aria-label attribute for better accessibility:

<LumexButton Variant="@Variant.Outlined"
             Class="min-w-10 w-10 h-10 p-0"
+            AriaLabel="Toggle theme"
             OnClick="@TriggerAsync"
             data-popoverref="@_dropdownId">

12-26: Consider adding error handling for async theme operations.

The dropdown structure and theme options are well-implemented. Consider adding try-catch blocks around the async theme operations to handle potential JavaScript interop failures gracefully:

-        <LumexDropdownItem OnClick="@(async () => await ThemeService.SetThemeAsync( Theme.Light ))">
+        <LumexDropdownItem OnClick="@(async () => { try { await ThemeService.SetThemeAsync( Theme.Light ); } catch { /* Log or handle error */ } })">

Apply similar error handling to the other menu items.

src/LumexUI/wwwroot/js/utils/theme.js (1)

7-21: Consider adding error handling for localStorage access.

The initialize function is well-designed with automatic system theme change detection. Consider adding try-catch blocks around localStorage operations to handle cases where storage is disabled or in private browsing mode:

function initialize() {
-    const initialTheme = get();
+    let initialTheme;
+    try {
+        initialTheme = get();
+    } catch {
+        initialTheme = "system";
+    }

    applyTheme(initialTheme);
    // ... rest of function
}
docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/DarkMode.razor (1)

50-57: Fix heading structure to match actual sections.

The headings array includes a "Setup" heading, but the actual sections are "Add the theme provider" and "Add a mode toggle". Update the headings to match the actual section structure:

new("Mode toggling", [
-    new("Setup"),
+    new("Add the theme provider"),
    new("Add a mode toggle"),
]),
src/LumexUI/Services/Theme/ThemeService.cs (1)

42-50: Consider adding error handling for JavaScript interop.

The initialization logic is sound with proper state management. Consider adding try-catch blocks around JavaScript interop calls to handle potential module loading failures or JavaScript errors gracefully:

public async ValueTask InitializeAsync()
{
-    var module = await _moduleTask.Value;
-    var theme = await module.InvokeAsync<string>( $"{JavaScriptPrefix}.initialize" );
+    try
+    {
+        var module = await _moduleTask.Value;
+        var theme = await module.InvokeAsync<string>( $"{JavaScriptPrefix}.initialize" );
+        
+        _currentTheme = TryParseTheme( theme );
+        await UpdateIsDarkModeAsync( _currentTheme );
+    }
+    catch
+    {
+        // Fallback to system theme if JavaScript fails
+        _currentTheme = Theme.System;
+        await UpdateIsDarkModeAsync( _currentTheme );
+    }
-    
-    _currentTheme = TryParseTheme( theme );
-    await UpdateIsDarkModeAsync( _currentTheme );
}
tests/LumexUI.Tests/Services/ThemeServiceTests.cs (2)

68-88: Fix the misleading test method name.

The test name suggests it should return Light theme, but the test actually mocks the JS to return "dark" and asserts for Theme.Dark. The test logic is correct, but the name is misleading.

Apply this diff to fix the test name:

-	public async Task GetThemeAsync_ShouldReturnLight_AndSetIsDarkModeTrue()
+	public async Task GetThemeAsync_ShouldReturnDark_AndSetIsDarkModeTrue()

210-214: Add null check for better error handling.

The helper method should validate that the reflection field lookup succeeds to provide clearer error messages if the internal structure changes.

Apply this diff to improve error handling:

private static void InjectModule( Task<IJSObjectReference> moduleTask, ThemeService service )
{
	var field = typeof( ThemeService ).GetField( "_moduleTask", BindingFlags.NonPublic | BindingFlags.Instance );
+	if( field == null )
+		throw new InvalidOperationException( "Unable to find '_moduleTask' field in ThemeService. The internal structure may have changed." );
	field?.SetValue( service, new Lazy<Task<IJSObjectReference>>( () => moduleTask ) );
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edc436b and e9ca5b7.

📒 Files selected for processing (24)
  • docs/LumexUI.Docs.Client/Common/Enums/PageStatus.cs (1 hunks)
  • docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs (2 hunks)
  • docs/LumexUI.Docs.Client/Common/Navigation/Types.cs (1 hunks)
  • docs/LumexUI.Docs.Client/Components/Layouts/MainLayout.razor (0 hunks)
  • docs/LumexUI.Docs.Client/Components/NavItemBadge.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Components/NavMenu.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Theming/Customization/Customization.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/DarkMode.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/ThemeToggle.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/ThemeTogglePreview.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeProvider.html (1 hunks)
  • docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeRoot.html (1 hunks)
  • docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeToggle.html (1 hunks)
  • docs/LumexUI.Docs.Client/_Imports.razor (1 hunks)
  • docs/LumexUI.Docs/Components/App.razor (1 hunks)
  • docs/LumexUI.Docs/Components/Routes.razor (1 hunks)
  • src/LumexUI/Common/Enums/Theme.cs (1 hunks)
  • src/LumexUI/Components/Providers/LumexThemeProvider.cs (1 hunks)
  • src/LumexUI/Components/Providers/LumexThemeProvider.razor.cs (0 hunks)
  • src/LumexUI/Extensions/ServiceCollectionExtensions.cs (3 hunks)
  • src/LumexUI/Services/Theme/ThemeService.cs (1 hunks)
  • src/LumexUI/wwwroot/js/components/popover.bundle.js (3 hunks)
  • src/LumexUI/wwwroot/js/utils/theme.js (1 hunks)
  • tests/LumexUI.Tests/Services/ThemeServiceTests.cs (1 hunks)
💤 Files with no reviewable changes (2)
  • docs/LumexUI.Docs.Client/Components/Layouts/MainLayout.razor
  • src/LumexUI/Components/Providers/LumexThemeProvider.razor.cs
🧰 Additional context used
🧠 Learnings (4)
docs/LumexUI.Docs.Client/_Imports.razor (1)

Learnt from: desmondinho
PR: #159
File: src/LumexUI.Motion/LumexUI.Motion.csproj:9-11
Timestamp: 2025-01-30T00:22:33.596Z
Learning: LumexUI.Motion is planned to be distributed as a separate NuGet package with its own test coverage, hence the assembly-wide coverage exclusion in the project.

src/LumexUI/Components/Providers/LumexThemeProvider.cs (1)

Learnt from: desmondinho
PR: #159
File: src/LumexUI.Motion/LumexUI.Motion.csproj:9-11
Timestamp: 2025-01-30T00:22:33.596Z
Learning: LumexUI.Motion is planned to be distributed as a separate NuGet package with its own test coverage, hence the assembly-wide coverage exclusion in the project.

src/LumexUI/Extensions/ServiceCollectionExtensions.cs (1)

Learnt from: desmondinho
PR: #159
File: src/LumexUI.Motion/LumexUI.Motion.csproj:9-11
Timestamp: 2025-01-30T00:22:33.596Z
Learning: LumexUI.Motion is planned to be distributed as a separate NuGet package with its own test coverage, hence the assembly-wide coverage exclusion in the project.

tests/LumexUI.Tests/Services/ThemeServiceTests.cs (1)

Learnt from: desmondinho
PR: #159
File: src/LumexUI.Motion/LumexUI.Motion.csproj:9-11
Timestamp: 2025-01-30T00:22:33.596Z
Learning: LumexUI.Motion is planned to be distributed as a separate NuGet package with its own test coverage, hence the assembly-wide coverage exclusion in the project.

🧬 Code Graph Analysis (2)
src/LumexUI/wwwroot/js/components/popover.bundle.js (2)
src/LumexUI/wwwroot/js/utils/dom.js (2)
  • createOutsideClickHandler (43-59)
  • clickHandler (44-52)
src/LumexUI/wwwroot/js/components/popover.js (9)
  • destroyOutsideClickHandler (20-20)
  • ref (25-25)
  • popover (24-24)
  • popover (109-112)
  • popover (109-112)
  • offsetVal (31-36)
  • middlewares (38-42)
  • data (60-63)
  • arrowElement (26-26)
src/LumexUI/Services/Theme/ThemeService.cs (2)
src/LumexUI/Components/Providers/LumexThemeProvider.cs (1)
  • Task (22-28)
src/LumexUI/Extensions/EnumExtensions.cs (1)
  • ToDescription (11-19)
🔇 Additional comments (45)
docs/LumexUI.Docs/Components/Routes.razor (1)

1-1: LGTM! Good architectural decision to centralize theme provider at routing level.

The placement of LumexThemeProvider before the Router component with @InteractiveWebAssembly rendermode ensures theme management is available throughout the application. This centralized approach is cleaner than having it in individual layouts.

docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeProvider.html (1)

1-2: LGTM! Clean documentation sample for theme provider usage.

The HTML-encoded Razor markup provides a clear, minimal example of how to instantiate the LumexThemeProvider component. The syntax highlighting setup with language-razor class is appropriate for documentation.

docs/LumexUI.Docs.Client/Components/NavMenu.razor (1)

53-53: Enum rename consistency verified

  • No occurrences of ComponentStatus remain in the codebase.
  • PageStatus is applied uniformly (e.g., in PageStatus.cs, NavigationItem, and NavigationStore).

All enum references have been updated correctly—no further changes needed.

docs/LumexUI.Docs.Client/Pages/Theming/Customization/Customization.razor (1)

41-41: LGTM! Improved separation of concerns with component change.

Replacing <ThemeTogglePreview /> with <CustomThemesPreview /> makes semantic sense - the custom themes section should demonstrate custom theme previews rather than the toggle mechanism. This aligns with the broader theming system reorganization where toggle-specific previews are likely moved to the dedicated dark mode documentation.

docs/LumexUI.Docs.Client/_Imports.razor (1)

19-19: LGTM! Necessary import for theming infrastructure.

Adding @using LumexUI.Services enables components in the docs client project to access the new ThemeService and other services without full namespace qualification. The placement among other LumexUI imports follows the existing organizational pattern.

docs/LumexUI.Docs.Client/Common/Enums/PageStatus.cs (1)

3-12: LGTM! Clean enum refactoring with good practices.

The renaming from ComponentStatus to PageStatus improves semantic clarity, and the addition of the Updated member extends functionality appropriately. The trailing comma after Preview follows good formatting practices for maintainability.

src/LumexUI/Common/Enums/Theme.cs (1)

12-31: Excellent enum design with comprehensive documentation.

The Theme enum is well-structured with:

  • Clear XML documentation for each member
  • Appropriate Description attributes using lowercase values (consistent with CSS/JavaScript conventions)
  • Logical theme options covering user preferences (Light/Dark) and system detection

This provides a solid foundation for the theming system.

docs/LumexUI.Docs.Client/Common/Navigation/Types.cs (1)

31-35: Consistent type updates following enum refactoring.

The parameter and property type changes from ComponentStatus? to PageStatus? correctly align with the enum rename, maintaining the same nullable semantics and ensuring type consistency across the navigation system.

src/LumexUI/Components/Providers/LumexThemeProvider.cs (1)

17-29: Well-implemented Blazor component following best practices.

The LumexThemeProvider correctly:

  • Extends ComponentBase for Blazor component functionality
  • Uses dependency injection with the [Inject] attribute and default! pattern
  • Implements OnAfterRenderAsync with firstRender check to prevent unnecessary re-initialization
  • Includes appropriate code coverage exclusion and XML documentation

The lifecycle management ensures theme initialization occurs only once after the component renders.

docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeRoot.html (1)

1-12: Clear and effective documentation sample.

This HTML sample effectively demonstrates the simplest approach to enable dark mode by adding class="dark" to the root <html> element. The code is properly structured as a documentation sample with:

  • Standard HTML5 doctype and meta tags
  • Correct module script loading for LumexUI.js
  • Clean, minimal example that's easy to understand and follow

This provides users with a straightforward implementation pattern for global dark mode.

docs/LumexUI.Docs.Client/Components/NavItemBadge.razor (2)

11-11: LGTM! Parameter type updated correctly.

The parameter type change from ComponentStatus? to PageStatus? aligns with the enum refactoring mentioned in the AI summary.


25-31: Dictionary mapping updated correctly with improved color contrast.

The dictionary correctly uses PageStatus enum keys, and the color change for Preview status from text-indigo-500 to text-indigo-600 provides better contrast. The addition of PageStatus.Updated status is well-integrated.

docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/ThemeTogglePreview.razor (1)

1-6: Well-structured preview component.

The component correctly uses InteractiveWebAssembly render mode for the theme toggle functionality and properly integrates with the PreviewCode component. The center justification enhances the visual presentation.

docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs (2)

18-20: Navigation items updated correctly for theming section.

The theming category properly reflects the new dark mode feature with appropriate status indicators. The "Dark Mode" item correctly uses PageStatus.New and "Customization" uses PageStatus.Updated.


25-50: Components section consistently updated with new enum.

All component navigation items have been systematically updated from ComponentStatus to PageStatus. The status assignments appear appropriate for the various components.

docs/LumexUI.Docs.Client/Samples/Theming/Dark Mode/darkModeToggle.html (2)

1-10: Component services and button implementation look correct.

The service injections are appropriate, and the button implementation with conditional icons based on dark mode CSS classes is well-designed. The data-popoverref attribute correctly links to the dropdown.


16-24: Theme setting implementation is correct.

The dropdown menu items properly use async calls to ThemeService.SetThemeAsync() with the appropriate Theme enum values. The implementation covers all three theme options (Light, Dark, System).

src/LumexUI/Extensions/ServiceCollectionExtensions.cs (2)

32-32: LGTM! Consistent service registration pattern.

The ThemeService registration follows the established pattern and is correctly added to both AddLumexServices overloads, ensuring the service is available regardless of configuration approach.

Also applies to: 45-45


78-81: LGTM! Proper service registration implementation.

The AddThemeService method correctly registers ThemeService as a scoped service, following the same pattern as other service registrations in this file. The scoped lifetime is appropriate for theme state management.

docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/ThemeToggle.razor (2)

1-2: LGTM! Appropriate service injections.

The component correctly injects the required services for theme management and popover functionality.


28-32: LGTM! Clean and focused implementation.

The code section is well-structured with a clear separation of concerns. The hardcoded ID is appropriate for a singleton theme toggle component.

src/LumexUI/wwwroot/js/utils/theme.js (6)

23-25: LGTM! Simple and effective with appropriate fallback.

The get function correctly retrieves the stored theme with a sensible default. The same localStorage error handling mentioned above would apply here as well.


27-30: LGTM! Clean implementation following persist-then-apply pattern.

The set function correctly stores and immediately applies the theme. The localStorage error handling suggestion from the initialize function would also apply here.


32-42: LGTM! Excellent toggle logic handling system theme intelligently.

The toggle function correctly handles all three theme states with intuitive behavior for the "system" theme - checking actual system preference and toggling to the opposite. The return value is useful for state synchronization.


44-46: LGTM! Standard implementation for system theme detection.

Correctly uses the standard matchMedia API to detect system dark mode preference.


48-59: LGTM! Comprehensive theme application with best practices.

The applyTheme function is well-structured, properly handling theme resolution and applying both CSS classes and the color-scheme property for optimal browser integration. The clean-slate approach prevents class conflicts.


61-67: LGTM! Clean module export with all necessary functions.

The export structure is well-organized and exposes all required theme management functions.

docs/LumexUI.Docs.Client/Pages/Theming/Dark Mode/DarkMode.razor (2)

1-12: LGTM! Clear documentation of global dark mode setup.

The route, layout, and explanation of global dark mode are appropriate and provide clear guidance for users implementing dark mode.


41-44: LGTM! Practical demonstration with interactive preview.

The mode toggle section effectively combines explanation with an interactive preview component, providing users with both guidance and a working example.

src/LumexUI/Services/Theme/ThemeService.cs (5)

15-36: LGTM! Well-designed service class with proper resource management.

The class structure follows best practices with lazy JavaScript module loading, sealed modifier for a service class, and proper state tracking. The IAsyncDisposable implementation ensures proper resource cleanup.


56-65: LGTM! Consistent state management pattern.

The method follows the same reliable pattern as InitializeAsync with proper state synchronization. The same error handling suggestion applies here for JavaScript interop robustness.


72-84: LGTM! Efficient implementation with early return optimization.

Good use of early return to avoid unnecessary operations when the theme hasn't changed. The ToDescription() extension method correctly converts the enum for JavaScript interop. The same error handling suggestion applies here to ensure state consistency if JavaScript calls fail.


102-124: LGTM! Well-designed helper methods providing consistent state management.

The UpdateIsDarkModeAsync method correctly handles all theme cases including system preference detection, and TryParseTheme provides safe parsing with appropriate fallback. These methods ensure consistent behavior across the service.


127-134: LGTM! Proper async disposal implementation.

The disposal logic correctly checks if the module was created before disposing and follows the async disposal pattern properly. The explicit interface implementation is appropriate for a service class.

tests/LumexUI.Tests/Services/ThemeServiceTests.cs (7)

17-40: LGTM! Well-structured initialization test.

The test correctly mocks both JS calls and verifies the expected behavior when JavaScript returns dark theme.


42-66: LGTM! Comprehensive GetThemeAsync test.

The test correctly verifies both the theme retrieval and the IsDarkMode state update for system theme preference.


90-110: LGTM! Correct light theme test.

The test properly verifies the light theme retrieval and corresponding IsDarkMode state.


112-135: LGTM! Excellent SetThemeAsync test with parameter validation.

The test properly validates that the correct theme value is passed to JavaScript and verifies the IsDarkMode state update.


137-162: LGTM! Important optimization test.

This test correctly verifies that redundant theme setting calls are optimized to avoid unnecessary JavaScript interop calls, which is crucial for performance.


164-183: LGTM! Clean toggle theme test.

The test correctly verifies the theme toggle functionality and the corresponding IsDarkMode state update.


185-208: LGTM! Proper disposal test.

The test correctly verifies that the ThemeService properly disposes of its JavaScript module, ensuring proper resource cleanup.

src/LumexUI/wwwroot/js/components/popover.bundle.js (4)

1483-1490: LGTM! Enhanced outside click detection for multiple elements.

The updated function now properly handles multiple elements (reference and popover), which correctly prevents the popover from closing when clicking on either element.


1515-1549: LGTM! Improved initialization logic and middleware ordering.

The changes correctly:

  • Pass both reference and popover elements to the outside click handler
  • Move the offset middleware earlier in the array for proper positioning sequence
  • Clean up variable destructuring for better readability

The middleware reordering (offset before flip/shift) ensures proper positioning calculations.


1554-1585: LGTM! Cleaner arrow positioning with improved parameter naming.

The changes improve the code by:

  • Simplifying the positionArrow function signature to accept only necessary parameters
  • Renaming target to popover for better clarity
  • Explicitly clearing conflicting right and bottom styles to prevent positioning issues

The internal derivation of placement and static side makes the function more self-contained.


1557-1557: LGTM! More specific error message.

The error message now correctly identifies the context as "popover.initialize" instead of a generic message, which improves debugging experience.

@desmondinho desmondinho merged commit ee1c390 into dev Jul 23, 2025
4 checks passed
@desmondinho desmondinho deleted the feat/dark-mode-toggle branch July 23, 2025 19:58
desmondinho added a commit that referenced this pull request Aug 24, 2025
* feat: support Tailwind CSS v4 (#183)

* build(deps): bump TailwindMerge.NET from 0.3.0 to 1.0.0

* feat: add new CSS theme file

* feat/build: add custom targets file to improve library usability

* chore: move `Plugin` folder one level higher; remove `Scripts` folder

* feat/build: pack new theme and custom `.targets` files

* build(docs): add `Directory.Build.props` and `Directory.Build.targets`

* chore(docs): remove tailwind npm deps; use standalone CLI instead

* docs: apply `static` on theme

* refactor(theme-provider): simplify names of box-shadow css variables

* refactor(theme-provider): opacities are percentage to match `color-mix` function syntax

* chore(components): apply  `static` on theme; fix some vars

* refactor(components): drop Tailwind CSS v3 support

* feat(theme): add custom transition variables

* fix(theme): correct `default` color; add `default-foreground` color

* chore(theme): add reference for the custom transitions approach (it's not in docs afaik)

* fix: rename `shadow-sm` to `shadow-xs`

* fix: rename `rounded-sm` to `rounded-xs`

* fix: rename `rounded` to `rounded-sm`

* fix: rename `outline-none` to `outline-hidden`

* fix: rename `ring-1` to `ring`

* fix(button): add base `cursor-pointer` class

* docs: remove `children` custom variant in favor of `*`

* fix(theme): correct `enter` custom animation

* chore(components): cleanup styles

* fix(theme): add missing comma separator in custom transition vars

* docs: configure typography

* refactor(theme): simplify `scrollbar-hide` utility

* chore(theme): apply `inline` on theme

* refactor: replace `theme` function with CSS vars

* fix(components): correct scale/translate transitions

* feat(theme): update colors from hex to oklch

* docs(installation): update installation guide

* feat(theme): add leading CSS vars

* chore(docs): fix prose `<code>` tag ticks

* refactor(utils): remove hex luminance calculator

* docs(customization): update Theme and Colors pages

* docs(colors): remove 'common colors are not configurable' callout

* fix(checkbox): correct radius styles

* fix(data-grid): correct striped styles

* fix(input/select): correct label placement out transitions

* fix(input/select): correct outlined variant focus styles

* fix(input): add cursor-pointer style on the clear button

* fix(input/select): correct flat variant focus styles

* fix(docs): correct some component examples

* build(docs): adjust Tailwind standalone CLI file download for Linux

* build(docs): adjust Tailwind standalone CLI file download for Linux

* ci(deploy): try add staging env in the ci/cd

* ci(build-test): change trigger branch

* ci(deploy): update trigger branches

* ci(deploy): change env vars usage (test)

* ci: add deploy-dev.yml; revert deploy.yml

* ci(deploy): test staging

* chore(docs): nits

* chore(components): tweak styles of some components

* chore(docs): tweak some components examples

* chore: coderabbit comments

* ci: remove deploy-dev.yml

* fix(theme): remove extra shade (950) from the color scales for consistency in dark mode (#199)

* fix(theme): remove extra key (950) from the color scales for consistency in dark mode

* build(docs): explicitly set Tailwind v4.0.9

* feat(components): introduce Avatar and AvatarGroup components (#201)

* feat: add baseline implementation

* feat: add slots

* feat: add basic slots styles

* feat: add appearance params, such as `Color`, `Radius`, `Size`

* feat: add `Bordered` and `Disabled` params

* feat: add compound variants styles

* feat: apply slots styles

* docs: add baseline examples page

* feat: add `data-loaded` attribute on img

* feat: add `ShowFallback` parameter

* chore: fix compound style variants

* chore: set `showFallback` on after first render

* feat(utils): add implicit cast to string for the `ElementClass`

* feat: add LumexAvatarGroup component

* feat: take into account when LumexAvatar is rendered inside the LumexAvatarGroup

* feat: add `AvatarClasses` parameter in the avatar group component

* docs: add Avatar page

* build(docs): explicitly set Tailwind v4.0.9

* test: add tests for LumexAvatar and LumexAvatarGroup components

* chore: simplify condition for fallback render

* fix(docs): replace usages of `-foreground-950` CSS classes with `-foreground-900`

* fix(docs): remove `dark:prose-invert` CSS class until dark theme is properly configured

* feat(components): introduce Skeleton component (#202)

* feat(skeleton): add baseline implementation of the component

* feat(skeleton): add slots and styles

* feat(skeleton): add XML summaries

* fix(skeleton): return back `after` pseudo CSS classes to prevent flickering on state change

* docs(skeleton): add Skeleton page

* test(skeleton): add tests

* docs(skeleton): fix Loading example button text

* fix(navbar): add a check before toggling navbar menu on navigation (#204)

* feat(components): introduce Spinner component (#207)

* feat(spinner): add baseline implementation

* feat(spinner): add variants and styles

* feat(spinner): add slots

* docs(spinner): add Spinner page

* docs: nits

* test(spinner): add tests

* docs: map static assets

* build(docs): remove extra MSBuild target for the Tailwind prod build

* docs: revert static assets changes

* docs: update static assets usage

* Revert "docs: update static assets usage"

This reverts commit 94ae9ec.

* feat(components): introduce Chip component (#211)

* feat(chip): add baseline implementation

* feat(chip): add ChipVariant enum

* feat(chip): add appearance parameters and styles

* feat(chip): add AvatarContent parameter

* feat(chip): adjust paddings when chip has start/end content

* docs(chip): add Chip page

* feat(chip): add XML summaries

* test(chip): add tests

* chore(components): add missing XML documentation summaries

* feat(components): add new Badge component (#222)

* feat(badge): initial

* feat(badge): add badge slots

* feat(badge): add badge baseline implementation

* feat(badge): add base visual-related params

* feat(badge): add majority of badge styles

* feat(badge): add outline around badge

* feat(badge): add `Invisible` param to control badge visibility

* feat(badge): add `IsOneChar` param to make badge equilateral

* feat(badge): decrease badge dimensions if no content provided

* refactor(badge): rename `IsOneChar` param to `OneChar`

* fix(badge): use correct type for `Variant` param

* feat(badge): apply CSS classes directly to the badge slot

* fix(badge): ensure correct placement styles

* fix(badge): correct `Content` check condition

* fix(badge): allow null for content

* fix(badge): properly render Content as RenderFragment

* docs(badge): add Badge docs page

* test(badge): add tests

* test(badge): add more tests

* fix(badge): ensure `OneChar` param is taken into account

* fix(badge): fix one char switch

* chore: apply CodeRabbit suggestions

* build(deps): bump requests from 2.32.0 to 2.32.4 in /scripts (#219)

Bumps [requests](https://github.com/psf/requests) from 2.32.0 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](psf/requests@v2.32.0...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(components): add Tooltip component (#224)

* feat(tooltip): initial

* refactor(popover): introduce PopoverWrapper to simplify open state

* fix(popover): ensure arrow is positioned correctly

* fix(popover): ensure popover closes on trigger click if already opened

* fix(popover): position flicker

* test(popover/dropdown): adjust tests

* refactor(popover): remove LastShown meta from popover service

* feat(tooltip): add baseline implementation

* feat(tooltip): add common visual-related parameters to pass into popover

* feat(tooltip): add OpenDelay and CloseDelay params

* docs(tooltip): add Tooltip page

* feat(tooltip): pass slots to popover

* chore(popover): add full radius styles

* test(tooltip): add tests

* docs(tooltip): minor tweaks

* docs(tooltip): typo

* feat(components): add Alert component (#225)

* feat(alert): add baseline implementation

* feat(alert): add styles (may be not complete)

* feat(alert): complete styles

* feat(button): add full radius styles

* feat(alert): apply styles

* feat(alert): add close button handler

* chore(alert): complete XML docs summaries

* chore(docs): adjust background colors for preview and toolbar

* chore(components): darken text color for warning flat variant

* chore(alert): styling tweaks

* feat(docs): add Alert page

* chore(alert): add missing close-button slot attribute

* chore(alert): ensure TitleContent takes precedence over Title

* docs(alert): add callout regarding Title and TitleContent parameters usage

* test(alert): add tests

* fix(theme): add tw custom CSS class-based dark mode variant (#226)

* refactor(theme): replace C# theme config with a new CSS-first approach (#229)

* feat(theme): add light and dark theme CSS files

* refactor(components): cleanup theme provider

* refactor(theme): remove Theme directory

* test(theme): remove Theme directory

* docs(theme): remove theme config

* fix(alert): correct RenderFragment parameters usage

* chore(theme): correct theme variables

* docs(customization): replace Customization section with Theming

* test(theme-provider): remove all tests

* feat(theme): introduce a mechanism to toggle light/dark modes (#230)

* feat(theme): introduce Theme service to manage and persist theme settings (JS)

* feat(theme): introduce Theme service to manage and persist theme settings

* docs(*): rename ComponentStatus enum to PageStatus

* docs(theming): add Dark Mode page

* chore(docs): component rename

* feat(button): add new `IconOnly` parameter

* docs(button): add demo for `IconOnly` parameter

* refactor(*): remove all bundled Google Material Icons and related code (#232)

* build: add new shared icons project

* feat(icons): add base icon component

* feat(icons): add dynamic icon component

* feat(icons): add some icons

* build(deps): reference icons in components

* refactor(accordion): use new icon components

* docs(components): use new icons in Callout components

* docs(*): use new icons

* refactor(icons): add "Icon" suffix; add more icons

* docs(*): use new icons

* test(*): fix tests

* refactor(components): remove `LumexIcon` component

* docs(datagrid): formatting

* refactor(icons): remove all Google Material Icons

* refactor(icons): remove script for downloading/updating icons

* feat(components): add dark mode support (#234)

* feat(alert): add dark mode support

* feat(button): add dark mode support

* feat(chip): add dark mode support

* feat(datagrid): add dark mode support

* feat(textbox/numbox): add dark mode support

* feat(listbox): add dark mode support

* feat(menu): add dark mode support

* feat(select): add dark mode support

* feat(tabs): add dark mode support

* fix(theme): ensure default values are of correct shade in dark mode

* feat(theme): add `color-scheme` in light theme

* fix(icons): use better icons for alert component

* docs: add dark mode support + theme toggle (#235)

* docs: add dark mode support

* docs: add missing border for the preview component

* docs: remove extra border in the preview code component

* chore(badge): improve flat variant contrast in light theme

* chore(tabs): remove `EditorRequired` attribute from `Id` param

* docs: add null check and theme class cleanup to prevent issues

* docs: remove IPopoverService injection from theme toggle component

* chore(components): remove unused / redundant types

* fix(data-grid): correct outside click handler creation

* feat(popover): enable position autoUpdate

* refactor(popover): make use of popover trigger component instead of service

* feat(dropdown): introduce dropdown trigger component instead of relying on popover service

* docs(dark-mode): update theme toggle dropdown example

* docs: add Home page with library usage examples (#241)

* feat(icons): add more icons

* docs: add showcases on home page

* chore(components): adjust some styles

* docs(overview): update paths

* chore(docs): nits

* fix(components): add missing popover js parts

* chore(docs): nits

* fix(popover): use overlay to close instead of custom outside click event

* fix(tooltip): remove pressed effect from on trigger hover

* chore(showcases): complete column visibility toggling in UserTable example

* chore(showcases): adjust legend color in usage chart

* test(popover): update tests

* chore(docs): coderabbit suggestions

* chore(*): migrate to DigitalOcean app platform

* fix(*): update custom LumexUI targets to copy theme files before build

* chore(*): add missing css files in the pack

* v2.0.0-preview.4

* perf(*): optimize fonts in docs app

* fix(docs): stylesheets import ordering

* perf(docs): optimize docs CSS output

* fix(docs): correct linux tailwind executable file name

* chore(*): delete update-icons.yml

* perf(docs): enable stream rendering on all pages

* fix(docs): ensure initial theme is set

* docs(overview): update content

* docs(installation): update content

* docs(design-tokens): update content

* docs(customization): update content

* docs(dark-mode): update content

* docs(accordion): update content

* refactor(docs): replace `Callout` with `LumexAlert`

* docs(avatar): update content

* docs(components): replace `Code` component usages with HTML tag

* fix(docs): ensure ThemeSelector sets correct theme value on init

* docs(landing): make adaptive

* docs(customization): add dark mode for global theme sample

* refactor(components): remove deprecated `Root` slot

* refactor(utils): remove redundant; update access modifiers

* docs(card): update slot names

* refactor(docs): make stream rendering global

* docs(header): remove active state from links; update stars counter

* ci(*): remove deploy.yml

* ci: run build-test on PR to main

* v2.0.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@coderabbitai coderabbitai bot mentioned this pull request Feb 22, 2026
5 tasks
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.

Add a method to toggle the light/dark modes [Feature]: Implement Dark Mode and toggle on the documentation site

1 participant