Compare commits

...

3 Commits

Author SHA1 Message Date
OlgunR
8933deec96 Add dark mode override for non-native DevExpress themes
Implements a dark mode override system for DevExpress Blazor themes lacking native dark support. Adds a JS function to toggle a dx-dark class on <html>, updates ThemeState to detect native dark themes, and applies targeted CSS variable overrides for consistent dark styling. Disables prerendering to ensure JS interop, and improves theme switching logic and documentation.
2026-05-05 16:41:15 +02:00
OlgunR
2010673eba Remove unused menu state and toggle logic from NavMenu
Removed the private menuOpen field and ToggleMenu() method from NavMenu.razor, as they are no longer needed for menu state management.
2026-04-30 15:41:05 +02:00
OlgunR
b75e7d730c Modernize sidebar with DxTreeView and new responsive styles
Refactor NavMenu to use DevExpress DxTreeView for navigation, replacing the old NavLink-based menu. Update sidebar and navigation row styling to use CSS variables, remove Bootstrap-specific and SVG icon CSS, and add a responsive hamburger menu for small screens. Improve dark mode support and overall maintainability.
2026-04-30 15:37:04 +02:00
10 changed files with 221 additions and 138 deletions

View File

@@ -24,6 +24,14 @@
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<script src="js/size-manager.js"></script>
<script>
window.setDxDarkOverride = function (enabled) {
if (enabled)
document.documentElement.classList.add('dx-dark');
else
document.documentElement.classList.remove('dx-dark');
};
</script>
<HeadOutlet />
</head>

View File

@@ -1,8 +1,9 @@
@inherits LayoutComponentBase
@implements IDisposable
@inject ThemeState ThemeState
@inject IJSRuntime JS
<div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light")">
<div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light") @(ThemeState.IsNativeDarkTheme ? "native-dark" : "")">
<div class="sidebar">
<NavMenu />
</div>
@@ -33,9 +34,43 @@
</div>
@code {
private bool _isInteractive;
protected override void OnInitialized()
{
ThemeState.OnChange += StateHasChanged;
ThemeState.OnChange += OnThemeChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_isInteractive = true;
}
await ApplyDxDarkOverrideAsync();
}
private async void OnThemeChanged()
{
StateHasChanged();
if (_isInteractive)
{
await ApplyDxDarkOverrideAsync();
}
}
private async Task ApplyDxDarkOverrideAsync()
{
if (!_isInteractive) return;
try
{
bool needsOverride = ThemeState.IsDarkMode && !ThemeState.IsNativeDarkTheme;
await JS.InvokeVoidAsync("setDxDarkOverride", needsOverride);
}
catch (JSException)
{
// JS-Funktion noch nicht verfügbar kein Circuit-Crash
}
}
private void ToggleTheme()
@@ -45,6 +80,6 @@
public void Dispose()
{
ThemeState.OnChange -= StateHasChanged;
ThemeState.OnChange -= OnThemeChanged;
}
}

View File

@@ -18,11 +18,13 @@ main {
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
background-color: var(--dx-color-surface-container, #f4f4f4);
border-right: 1px solid var(--dx-color-outline-variant, #e0e0e0);
}
.page.app-dark .sidebar {
background-image: linear-gradient(180deg, #171717 0%, #0f2a46 70%);
background-color: var(--dx-color-surface-container, #1e1e1e);
border-right-color: var(--dx-color-outline-variant, #333);
}
.top-row {

View File

@@ -1,36 +1,24 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">DbFirst.BlazorWebApp</a>
</div>
<div class="nav-brand-row">
<a class="nav-brand-link" href="">DbFirst</a>
<button class="nav-toggle-btn" @onclick="ToggleMenu" title="Navigation menu">&#9776;</button>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="catalogs">
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards">
<span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
</NavLink>
</div>
</nav>
<div class="nav-scrollable @(menuOpen ? "nav-open" : "")">
<DxTreeView CssClass="sidebar-tree">
<Nodes>
<DxTreeViewNode Text="Home" NavigateUrl="/" IconCssClass="dxi dxi-home" />
<DxTreeViewNode Text="Data Management" Expanded="true">
<Nodes>
<DxTreeViewNode Text="Catalogs" NavigateUrl="/catalogs" IconCssClass="dxi dxi-folder" />
<DxTreeViewNode Text="Dashboards" NavigateUrl="/dashboards" IconCssClass="dxi dxi-chart-bar" />
<DxTreeViewNode Text="Mass Data" NavigateUrl="/massdata" IconCssClass="dxi dxi-table" />
</Nodes>
</DxTreeViewNode>
</Nodes>
</DxTreeView>
</div>
@code {
private bool menuOpen = false;
private void ToggleMenu() => menuOpen = !menuOpen;
}

View File

@@ -1,117 +1,52 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
.nav-brand-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.navbar-brand {
font-size: 1.1rem;
.nav-brand-link {
font-size: 1.05rem;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-collection-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M2 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v1H2V3z'/%3E%3Cpath d='M2 5h12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5z'/%3E%3C/svg%3E");
}
.bi-speedometer-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 11a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v4zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v8zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v2z'/%3E%3C/svg%3E");
}
.bi-table-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm1 1v2h12V3H2zm12 3H2v2h12V6zm0 3H2v2h12V9zm0 3H2v1h12v-1z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
.nav-toggle-btn {
display: block;
background: none;
border: none;
font-size: 1.4rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
line-height: 1;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
.nav-scrollable.nav-open {
display: block;
}
.sidebar-tree {
width: 100%;
}
@media (min-width: 641px) {
.navbar-toggler {
.nav-toggle-btn {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -1,4 +1,5 @@
@rendermode InteractiveServer
@rendermode @(new InteractiveServerRenderMode(prerender: false))
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">

View File

@@ -14,6 +14,16 @@ public class ThemeState
public bool IsDarkMode { get; private set; }
public string CurrentThemeName { get; private set; } = "Fluent";
/// <summary>
/// Themes die eine native DevExpress Dark-Variante besitzen:
/// - Fluent ? Themes.Fluent.Clone(ThemeMode.Dark), verwendet --DS-* Token-System
/// - BlazingBerry ? Themes.BlazingDark
/// Alle anderen Themes (Purple, OfficeWhite, BootstrapExternal) haben keine offizielle
/// Dark-Variante; dort übernehmen CSS-Overrides auf --dxbl-grid-* Variablen die Arbeit.
/// </summary>
public bool IsNativeDarkTheme => IsDarkMode &&
(CurrentThemeName == "Fluent" || CurrentThemeName == "BlazingBerry");
public static readonly List<string> AvailableThemes = ["Fluent", "BlazingBerry", "Purple", "OfficeWhite", "BootstrapExternal"];
public event Action? OnChange;
@@ -45,13 +55,11 @@ public class ThemeState
});
themeChangeService.SetTheme(theme);
}
else if (CurrentThemeName == "BlazingBerry") themeChangeService.SetTheme(Themes.BlazingBerry);
else if (CurrentThemeName == "Purple") themeChangeService.SetTheme(Themes.Purple);
else if (CurrentThemeName == "OfficeWhite") themeChangeService.SetTheme(Themes.OfficeWhite);
else if (CurrentThemeName == "BlazingBerry") themeChangeService.SetTheme(IsDarkMode ? Themes.BlazingDark : Themes.BlazingBerry);
else if (CurrentThemeName == "Purple") themeChangeService.SetTheme(Themes.Purple);
else if (CurrentThemeName == "OfficeWhite") themeChangeService.SetTheme(Themes.OfficeWhite);
else if (CurrentThemeName == "BootstrapExternal") themeChangeService.SetTheme(Themes.BootstrapExternal);
else
themeChangeService.SetTheme(Themes.Fluent);
OnChange?.Invoke();
}
}

View File

@@ -5,5 +5,11 @@
"Microsoft.AspNetCore": "Warning"
}
},
"ApiBaseUrl": "https://localhost:7204/"
"ApiBaseUrl": "https://localhost:7204/",
"BrowserLink": {
"Enabled": false
},
"DetailedErrors": true
}

View File

@@ -148,6 +148,105 @@ dxbl-grid tbody tr:nth-child(even) td {
background-color: var(--grid-stripe-bg) !important;
}
/* ?? Dark-Mode-Overrides für nicht-native Themes ?????????????????????????????
Strategie: CSS-Custom-Properties werden von DevExpress DIREKT auf den
Komponenten-Elementen definiert (z. B. --dxbl-popup-bg:#fff auf .dxbl-modal).
Eine geerbte Variable aus html.dx-dark würde durch die direkte Zuweisung
überschrieben. Deshalb targeten wir exakt dieselben Elemente, aber mit einem
zusätzlichen Vorfahren-Selektor (html.dx-dark) für höhere Spezifizität:
html.dx-dark .dxbl-modal = (0,2,1) > .dxbl-modal = (0,1,0) ?
html.dx-dark wird per JS gesetzt, wenn IsDarkMode && !IsNativeDarkTheme.
?? */
/* Popup / Modal (CRUD-Dialoge) Variablen-Quelle: .dxbl-modal */
html.dx-dark .dxbl-modal {
--dxbl-popup-bg: #2d2d2d;
--dxbl-popup-color: #e8e8e8;
--dxbl-popup-border-color: #555;
--dxbl-popup-header-bg: #333;
--dxbl-popup-header-color: #e8e8e8;
--dxbl-popup-footer-bg: #333;
--dxbl-popup-footer-color: #e8e8e8;
}
/* Flyout (Column Chooser, Filter-Panel) Variablen-Quelle: .dxbl-flyout */
html.dx-dark .dxbl-flyout {
--dxbl-flyout-bg: #2d2d2d;
--dxbl-flyout-color: #e8e8e8;
--dxbl-flyout-border-color: #555;
--dxbl-flyout-header-bg: #333;
--dxbl-flyout-header-color: #e8e8e8;
--dxbl-flyout-footer-bg: #333;
}
/* Dropdown (ComboBox-Klappliste, Band-Dropdowns) Quelle: .dxbl-dropdown */
html.dx-dark .dxbl-dropdown,
html.dx-dark .dxbl-itemlist-dropdown {
--dxbl-dropdown-bg: #2d2d2d;
--dxbl-dropdown-color: #e8e8e8;
--dxbl-dropdown-border-color: #555;
--dxbl-dropdown-header-bg: #333;
--dxbl-dropdown-footer-bg: #333;
}
/* Edit-Dropdown (ComboBox-Popup wenn als Modal gerendert) Quelle: .dxbl-edit-dropdown */
html.dx-dark .dxbl-edit-dropdown {
--dxbl-edit-dropdown-bg: #2d2d2d;
--dxbl-edit-dropdown-color: #e8e8e8;
--dxbl-edit-dropdown-border-color: #555;
}
/* ListBox (Einträge in Dropdowns) Quelle: .dxbl-list-box */
html.dx-dark .dxbl-list-box,
html.dx-dark .dxbl-list-box-render-container {
--dxbl-list-box-bg: #2d2d2d;
--dxbl-list-box-color: #e8e8e8;
--dxbl-list-box-border-color: #555;
--dxbl-list-box-item-hover-bg: #3a3a3a;
--dxbl-list-box-item-hover-color: #e8e8e8;
}
/* TextEdit / ComboBox Eingabefeld Quelle: .dxbl-text-edit */
html.dx-dark .dxbl-text-edit {
--dxbl-text-edit-bg: #2d2d2d;
--dxbl-text-edit-color: #e8e8e8;
--dxbl-text-edit-border-color: #555;
--dxbl-text-edit-btn-bg: #3a3a3a;
--dxbl-text-edit-btn-color: #e8e8e8;
--dxbl-text-edit-btn-hover-bg: #444;
--dxbl-text-edit-btn-hover-color: #e8e8e8;
}
/* Buttons */
html.dx-dark .dxbl-btn {
--dxbl-btn-color: #e8e8e8;
--dxbl-btn-bg: #3a3a3a;
--dxbl-btn-border-color: #555;
--dxbl-btn-hover-bg: #444;
--dxbl-btn-hover-color: #e8e8e8;
--dxbl-btn-hover-border-color: #666;
}
/* FormLayout */
html.dx-dark .dxbl-fl {
--dxbl-fl-caption-color: #bbb;
--dxbl-fl-group-bg: #242424;
--dxbl-fl-group-color: #e8e8e8;
}
/* Grid */
html.dx-dark .dxbl-grid {
background-color: #242424;
color: #e8e8e8;
border-color: #444;
}
html.dx-dark .dxbl-grid > .dxbl-scroll-viewer,
html.dx-dark .dxbl-grid > .dxbl-grid-top-panel {
background-color: #242424;
color: #e8e8e8;
}
/* MassData-spezifisch */
.page-size-selector {
display: flex;

View File

@@ -1,3 +1,4 @@
window.setSize = function (fontSize) {
document.documentElement.style.setProperty('--global-size', fontSize);
};