Compare commits

...

9 Commits

Author SHA1 Message Date
OlgunR
1112fa215c Sync date filter UI with filter criteria in BandGridBase
Added logic to synchronize the date filter UI with the current
filter criteria by updating _filterFrom and _filterTo based on
the CriteriaOperator. Introduced SyncDateFilterFromContext and
ParseDateOperand helpers to extract and apply "from" and "to"
date values, ensuring UI and filter state remain consistent.
2026-05-11 10:27:13 +02:00
OlgunR
f0259e3f78 Sync date filter UI with criteria; restore dropdown footer
Add _filterContexts to track filter menu contexts per field and update their FilterCriteria when criteria change, ensuring the date filter UI stays in sync. Remove CSS that hid the native Apply/Clear footer in the date filter dropdown.
2026-05-11 10:00:02 +02:00
OlgunR
d9785baf5b Add custom date range filter UI for grid date columns
Introduced a custom date range filter menu for date columns in BandGridBase<TItem> using DevExpress Blazor grids. The new UI provides "from" and "to" date pickers, applies filters immediately on selection, and hides the default filter dropdown footer for a smoother user experience. State management and filter criteria logic were added to support this feature.
2026-05-11 09:49:17 +02:00
OlgunR
9dc65ab92f Adjust dark theme dropdown colors and toolbar spacing
Added custom colors for .dxbl-btn-dropdown-popup in dark mode to improve dropdown item visibility. Also added Bootstrap me-2 class to the "Spalten" toolbar button for better spacing.
2026-05-08 11:06:55 +02:00
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
OlgunR
075433c780 Improve spacing for Dark Mode button in top row
Wrapped the Dark Mode toggle button in a span with left margin
for better separation from the theme combo box. Added a new
.btn-gap CSS class to standardize button spacing in the top row.
2026-04-23 15:59:50 +02:00
OlgunR
35e39ff979 Add theme selection dropdown and refactor theme handling
Introduce a DxComboBox in MainLayout for selecting between multiple themes. Update ThemeState to manage the current theme, provide a list of available themes, and apply the selected theme via a new SetTheme method. Refactor dark mode handling to work with the new theme system, and ensure UI updates on theme or mode changes.
2026-04-23 15:44:26 +02:00
12 changed files with 409 additions and 150 deletions

View File

@@ -24,6 +24,14 @@
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" /> <link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<script src="js/size-manager.js"></script> <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 /> <HeadOutlet />
</head> </head>

View File

@@ -1,6 +1,7 @@
using DbFirst.BlazorWebApp.Models.Grid; using DbFirst.BlazorWebApp.Models.Grid;
using DbFirst.BlazorWebApp.Services; using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor; using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
@@ -24,6 +25,16 @@ public abstract class BandGridBase<TItem> : ComponentBase
protected bool gridLayoutApplied; protected bool gridLayoutApplied;
protected IGrid? gridRef; protected IGrid? gridRef;
// --- Datumsfilter-Zustand ---
private readonly Dictionary<string, DateTime?> _filterFrom = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, DateTime?> _filterTo = new(StringComparer.OrdinalIgnoreCase);
// Stabile Referenzen: werden einmal pro FieldName erstellt und wiederverwendet
private readonly Dictionary<string, EventCallback<DateTime?>> _fromCallbacks = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, EventCallback<DateTime?>> _toCallbacks = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, RenderFragment<GridDataColumnFilterMenuTemplateContext>> _dateFilterTemplates = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, GridDataColumnFilterMenuTemplateContext> _filterContexts = new(StringComparer.OrdinalIgnoreCase);
// --- SizeMode --- // --- SizeMode ---
protected SizeMode _sizeMode = SizeMode.Medium; protected SizeMode _sizeMode = SizeMode.Medium;
protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList(); protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
@@ -224,9 +235,134 @@ public abstract class BandGridBase<TItem> : ComponentBase
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat); builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly) if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true); builder.AddAttribute(seq++, "ReadOnly", true);
if (column.FilterType == ColumnFilterType.Date)
builder.AddAttribute(seq++, "FilterMenuTemplate", GetOrCreateDateFilterTemplate(column.FieldName));
builder.CloseComponent(); builder.CloseComponent();
} }
private RenderFragment<GridDataColumnFilterMenuTemplateContext> GetOrCreateDateFilterTemplate(string fieldName)
{
if (!_dateFilterTemplates.TryGetValue(fieldName, out var template))
{
// EventCallbacks einmalig erstellen stabile Referenzen über alle Renders
_fromCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, (DateTime? v) => OnFilterFromChanged(fieldName, v));
_toCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, (DateTime? v) => OnFilterToChanged(fieldName, v));
template = BuildDateFilterTemplate(fieldName);
_dateFilterTemplates[fieldName] = template;
}
return template;
}
private RenderFragment<GridDataColumnFilterMenuTemplateContext> BuildDateFilterTemplate(string fieldName) =>
ctx => b =>
{
_filterContexts[fieldName] = ctx;
SyncDateFilterFromContext(fieldName, ctx.FilterCriteria);
int s = 0;
b.OpenElement(s++, "div");
b.AddAttribute(s++, "class", "date-filter-menu p-2");
// Ab Datum
b.OpenElement(s++, "div");
b.AddAttribute(s++, "class", "mb-2");
b.OpenElement(s++, "label");
b.AddAttribute(s++, "class", "form-label small fw-semibold");
b.AddContent(s++, "Ab Datum");
b.CloseElement();
b.OpenComponent<DxDateEdit<DateTime?>>(s++);
b.AddAttribute(s++, "Date", _filterFrom.GetValueOrDefault(fieldName));
b.AddAttribute(s++, "DateChanged", _fromCallbacks[fieldName]);
b.AddAttribute(s++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
b.AddAttribute(s++, "NullText", "Kein Startdatum");
b.AddAttribute(s++, "Width", "100%");
b.CloseComponent();
b.CloseElement();
// Bis Datum
b.OpenElement(s++, "div");
b.AddAttribute(s++, "class", "mb-0");
b.OpenElement(s++, "label");
b.AddAttribute(s++, "class", "form-label small fw-semibold");
b.AddContent(s++, "Bis Datum");
b.CloseElement();
b.OpenComponent<DxDateEdit<DateTime?>>(s++);
b.AddAttribute(s++, "Date", _filterTo.GetValueOrDefault(fieldName));
b.AddAttribute(s++, "DateChanged", _toCallbacks[fieldName]);
b.AddAttribute(s++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
b.AddAttribute(s++, "NullText", "Kein Enddatum");
b.AddAttribute(s++, "Width", "100%");
b.CloseComponent();
b.CloseElement();
b.CloseElement();
};
private void SyncDateFilterFromContext(string fieldName, CriteriaOperator? criteria)
{
if (criteria == null)
{
_filterFrom[fieldName] = null;
_filterTo[fieldName] = null;
return;
}
DateTime? from = null;
DateTime? to = null;
if (criteria is GroupOperator group)
{
foreach (var op in group.Operands.OfType<BinaryOperator>())
ParseDateOperand(op, ref from, ref to);
}
else if (criteria is BinaryOperator binary)
{
ParseDateOperand(binary, ref from, ref to);
}
_filterFrom[fieldName] = from;
_filterTo[fieldName] = to;
}
private static void ParseDateOperand(BinaryOperator op, ref DateTime? from, ref DateTime? to)
{
if (op.RightOperand is not OperandValue val || val.Value is not DateTime dt) return;
if (op.OperatorType == BinaryOperatorType.GreaterOrEqual) from = dt;
else if (op.OperatorType == BinaryOperatorType.Less) to = dt.AddDays(-1);
}
private void OnFilterFromChanged(string fieldName, DateTime? value)
{
_filterFrom[fieldName] = value;
ApplyDateFilter(fieldName);
}
private void OnFilterToChanged(string fieldName, DateTime? value)
{
_filterTo[fieldName] = value;
ApplyDateFilter(fieldName);
}
private void ApplyDateFilter(string fieldName)
{
var ops = new List<CriteriaOperator>();
if (_filterFrom.TryGetValue(fieldName, out var from) && from.HasValue)
ops.Add(new BinaryOperator(fieldName, from.Value.Date, BinaryOperatorType.GreaterOrEqual));
if (_filterTo.TryGetValue(fieldName, out var to) && to.HasValue)
ops.Add(new BinaryOperator(fieldName, to.Value.Date.AddDays(1), BinaryOperatorType.Less));
CriteriaOperator? criteria = ops.Count switch
{
0 => null,
1 => ops[0],
_ => new GroupOperator(GroupOperatorType.And, ops)
};
gridRef?.SetFieldFilterCriteria(fieldName, criteria);
if (_filterContexts.TryGetValue(fieldName, out var ctx))
ctx.FilterCriteria = criteria;
_ = InvokeAsync(StateHasChanged);
}
protected void SetEditContext(EditContext context) protected void SetEditContext(EditContext context)
{ {
if (editContext == context) return; if (editContext == context) return;

View File

@@ -87,7 +87,7 @@ else
Click="DeleteFocusedRow" /> Click="DeleteFocusedRow" />
</Template> </Template>
</DxToolbarItem> </DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right"> <DxToolbarItem Alignment="ToolbarItemAlignment.Right" CssClass="me-2">
<Template Context="_"> <Template Context="_">
<DxButton Text="Spalten" <DxButton Text="Spalten"
RenderStyle="ButtonRenderStyle.Secondary" RenderStyle="ButtonRenderStyle.Secondary"

View File

@@ -1,15 +1,23 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@implements IDisposable @implements IDisposable
@inject ThemeState ThemeState @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"> <div class="sidebar">
<NavMenu /> <NavMenu />
</div> </div>
<main> <main>
<div class="top-row px-4"> <div class="top-row px-4">
<DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")" Click="ToggleTheme" /> <DxComboBox Data="@ThemeState.AvailableThemes"
Value="@ThemeState.CurrentThemeName"
ValueChanged="@((string t) => ThemeState.SetTheme(t))"
style="width: 130px;" />
<span style="margin-left: 12px;">
<DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")"
Click="ToggleTheme" />
</span>
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div> </div>
@@ -26,9 +34,43 @@
</div> </div>
@code { @code {
private bool _isInteractive;
protected override void OnInitialized() 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() private void ToggleTheme()
@@ -38,6 +80,6 @@
public void Dispose() public void Dispose()
{ {
ThemeState.OnChange -= StateHasChanged; ThemeState.OnChange -= OnThemeChanged;
} }
} }

View File

@@ -18,11 +18,13 @@ main {
} }
.sidebar { .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 { .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 { .top-row {

View File

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

View File

@@ -1,117 +1,52 @@
.navbar-toggler { .nav-brand-row {
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 {
height: 3.5rem; height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.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; display: flex;
align-items: center; align-items: center;
line-height: 3rem; justify-content: space-between;
width: 100%; padding: 0 1rem;
} border-bottom: 1px solid rgba(0, 0, 0, 0.12);
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
} }
.nav-item ::deep .nav-link:hover { .nav-brand-link {
background-color: rgba(255,255,255,0.1); font-size: 1.05rem;
color: white; font-weight: 600;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.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 { .nav-scrollable {
display: none; display: none;
} }
.navbar-toggler:checked ~ .nav-scrollable { .nav-scrollable.nav-open {
display: block; display: block;
}
.sidebar-tree {
width: 100%;
} }
@media (min-width: 641px) { @media (min-width: 641px) {
.navbar-toggler { .nav-toggle-btn {
display: none; display: none;
} }
.nav-scrollable { .nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block; display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem); height: calc(100vh - 3.5rem);
overflow-y: auto; overflow-y: auto;
} }
} }

View File

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

View File

@@ -12,24 +12,54 @@ public class ThemeState
} }
public bool IsDarkMode { get; private set; } 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; public event Action? OnChange;
public void SetDarkMode(bool isDarkMode) public void SetTheme(string themeName)
{ {
if (IsDarkMode == isDarkMode) if (CurrentThemeName == themeName) return;
{ CurrentThemeName = themeName;
return; ApplyTheme();
}
IsDarkMode = isDarkMode;
var theme = Themes.Fluent.Clone(properties =>
{
properties.Mode = isDarkMode ? ThemeMode.Dark : ThemeMode.Light;
properties.ApplyToPageElements = true;
});
themeChangeService.SetTheme(theme);
OnChange?.Invoke(); OnChange?.Invoke();
} }
public void SetDarkMode(bool isDarkMode)
{
if (IsDarkMode == isDarkMode) return;
IsDarkMode = isDarkMode;
ApplyTheme();
OnChange?.Invoke();
}
private void ApplyTheme()
{
if (CurrentThemeName == "Fluent")
{
var theme = Themes.Fluent.Clone(properties =>
{
properties.Mode = IsDarkMode ? ThemeMode.Dark : ThemeMode.Light;
properties.ApplyToPageElements = true;
});
themeChangeService.SetTheme(theme);
}
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);
}
} }

View File

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

View File

@@ -148,6 +148,112 @@ dxbl-grid tbody tr:nth-child(even) td {
background-color: var(--grid-stripe-bg) !important; 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;
}
/* Button-Dropdown-Items (Sizemode-Dropdown, Band-Dropdowns) Quelle: .dxbl-btn-dropdown-popup */
html.dx-dark .dxbl-btn-dropdown-popup {
--dxbl-btn-dropdown-btn-color: #e8e8e8;
--dxbl-btn-dropdown-btn-hover-color: #e8e8e8;
--dxbl-btn-dropdown-btn-active-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 */ /* MassData-spezifisch */
.page-size-selector { .page-size-selector {
display: flex; display: flex;
@@ -185,3 +291,7 @@ dxbl-grid tbody tr:nth-child(even) td {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.top-row .btn-gap {
margin-left: 8px;
}

View File

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