From 7bd0edafe282f22d85c68ecfbf7262fc43a9d1b9 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 2 Oct 2026 23:50:40 +0200 Subject: [PATCH 1/7] fix: MultiSeriesChart render crash It redeclared ChildContent (already inherited from Chart) and passed Subtitle to ApexChart, which has no such parameter; either one threw on every render. It now also uses Chart's card container. --- .../Components/MultiSeriesChart.razor | 21 ++++++++----------- .../Templates/MultiSeriesChartTemplate.cs | 20 ++++++++---------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/ShellUI.Components/Components/MultiSeriesChart.razor b/src/ShellUI.Components/Components/MultiSeriesChart.razor index 6e2bc7c..d772d6e 100644 --- a/src/ShellUI.Components/Components/MultiSeriesChart.razor +++ b/src/ShellUI.Components/Components/MultiSeriesChart.razor @@ -3,15 +3,12 @@ @inherits Chart @typeparam TItem where TItem : class - - @ChildContent - - -@code { - [Parameter] public new RenderFragment? ChildContent { get; set; } -} \ No newline at end of file +
+ + @ChildContent + +
diff --git a/src/ShellUI.Templates/Templates/MultiSeriesChartTemplate.cs b/src/ShellUI.Templates/Templates/MultiSeriesChartTemplate.cs index 9fb9ac0..dc0ce3e 100644 --- a/src/ShellUI.Templates/Templates/MultiSeriesChartTemplate.cs +++ b/src/ShellUI.Templates/Templates/MultiSeriesChartTemplate.cs @@ -21,16 +21,14 @@ @using ApexCharts @inherits Chart @typeparam TItem where TItem : class - - @ChildContent - - -@code { - // ChildContent is inherited from Chart -} +
+ + @ChildContent + +
"; } \ No newline at end of file From a00e2f9dabba808a5271caa50c8c9c726aed71e2 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 2 Oct 2026 23:50:41 +0200 Subject: [PATCH 2/7] fix: define window.ShellUI for package users and add the missing JS functions The package's shellui.js is an ES module and only ran when a component happened to import it, so ThemeToggle, dialog scroll lock and close-on-scroll silently did nothing. A Blazor JS initializer now imports it at startup. Adds addClassToDocument, removeClassFromDocument and setupFileDrop, which ThemeToggle, ThemeService and FileUpload call. --- .../wwwroot/ShellUI.Components.lib.module.js | 2 ++ src/ShellUI.Components/wwwroot/shellui.js | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/ShellUI.Components/wwwroot/ShellUI.Components.lib.module.js diff --git a/src/ShellUI.Components/wwwroot/ShellUI.Components.lib.module.js b/src/ShellUI.Components/wwwroot/ShellUI.Components.lib.module.js new file mode 100644 index 0000000..fec66c0 --- /dev/null +++ b/src/ShellUI.Components/wwwroot/ShellUI.Components.lib.module.js @@ -0,0 +1,2 @@ +// Blazor loads this initializer automatically; importing shellui.js defines window.ShellUI for every package component. +import "./shellui.js"; diff --git a/src/ShellUI.Components/wwwroot/shellui.js b/src/ShellUI.Components/wwwroot/shellui.js index fa94a46..032c768 100644 --- a/src/ShellUI.Components/wwwroot/shellui.js +++ b/src/ShellUI.Components/wwwroot/shellui.js @@ -12,6 +12,33 @@ Object.assign(window.ShellUI, { if (element) element.focus(); }, + addClassToDocument: function (className) { + document.documentElement.classList.add(className); + }, + + removeClassFromDocument: function (className) { + document.documentElement.classList.remove(className); + }, + + setupFileDrop: function (dropZoneId, inputElementId) { + const dropZone = document.getElementById(dropZoneId); + const input = document.getElementById(inputElementId); + if (!dropZone || !input) return false; + if (dropZone._shelluiDrop) dropZone.removeEventListener("drop", dropZone._shelluiDrop); + dropZone._shelluiDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + const files = e.dataTransfer && e.dataTransfer.files; + if (!files || files.length === 0) return; + const dt = new DataTransfer(); + for (let i = 0; i < files.length; i++) dt.items.add(files[i]); + input.files = dt.files; + input.dispatchEvent(new Event("change", { bubbles: true })); + }; + dropZone.addEventListener("drop", dropZone._shelluiDrop); + return true; + }, + registerShortcut: function (handle, key, ctrl, meta, shift, alt, dotNetRef) { const listener = (e) => { if (e.key.toLowerCase() !== key.toLowerCase()) return; @@ -73,6 +100,9 @@ Object.assign(window.ShellUI, { // ES module re-exports so consumers who import this file dynamically still work. export function copyToClipboard(text) { return window.ShellUI.copyToClipboard(text); } export function focusElement(elementId) { return window.ShellUI.focusElement(elementId); } +export function addClassToDocument(className) { return window.ShellUI.addClassToDocument(className); } +export function removeClassFromDocument(className) { return window.ShellUI.removeClassFromDocument(className); } +export function setupFileDrop(dropZoneId, inputElementId) { return window.ShellUI.setupFileDrop(dropZoneId, inputElementId); } export function registerShortcut(handle, key, ctrl, meta, shift, alt, dotNetRef) { return window.ShellUI.registerShortcut(handle, key, ctrl, meta, shift, alt, dotNetRef); } From 1f952fff47f0d7780de621d754d83414adf63fd8 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 2 Oct 2026 23:50:44 +0200 Subject: [PATCH 3/7] fix: keep ThemeToggle in sync per page instead of across users The static instance list was shared by every circuit on Blazor Server. Each toggle now observes the page's dark class through ShellUI.observeTheme, which also reports the initial theme. --- .../Components/ThemeToggle.razor | 52 +++++++++++-------- src/ShellUI.Components/wwwroot/shellui.js | 21 ++++++++ .../Templates/ShellUIJsTemplate.cs | 20 +++++++ .../Templates/ThemeToggleTemplate.cs | 52 +++++++++++-------- 4 files changed, 101 insertions(+), 44 deletions(-) diff --git a/src/ShellUI.Components/Components/ThemeToggle.razor b/src/ShellUI.Components/Components/ThemeToggle.razor index 6f239f1..304283a 100644 --- a/src/ShellUI.Components/Components/ThemeToggle.razor +++ b/src/ShellUI.Components/Components/ThemeToggle.razor @@ -24,7 +24,9 @@ @code { - private static readonly List _instances = new(); + private readonly string _handle = Guid.NewGuid().ToString("N"); + private DotNetObjectReference? _selfRef; + private bool _observing; private bool _isDark = true; [Parameter] public string Size { get; set; } = "default"; @@ -33,25 +35,38 @@ [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } - protected override void OnInitialized() - { - _instances.Add(this); - } - // JSRuntime is unavailable during prerender. protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; try { - var theme = await JSRuntime.InvokeAsync("localStorage.getItem", "theme"); - _isDark = string.IsNullOrEmpty(theme) ? true : theme == "dark"; - StateHasChanged(); + // Reports the page's current theme and keeps every toggle in sync, whoever changes it. + _selfRef = DotNetObjectReference.Create(this); + _isDark = await JSRuntime.InvokeAsync("ShellUI.observeTheme", _handle, _selfRef); + _observing = true; } catch { - _isDark = true; + try + { + var theme = await JSRuntime.InvokeAsync("localStorage.getItem", "theme"); + _isDark = string.IsNullOrEmpty(theme) || theme == "dark"; + } + catch + { + return; + } } + StateHasChanged(); + } + + [JSInvokable] + public Task OnThemeChanged(bool isDark) + { + if (_isDark == isDark) return Task.CompletedTask; + _isDark = isDark; + return InvokeAsync(StateHasChanged); } private async Task ToggleTheme() @@ -65,16 +80,6 @@ await JSRuntime.InvokeVoidAsync( _isDark ? "ShellUI.addClassToDocument" : "ShellUI.removeClassFromDocument", "dark"); - - foreach (var instance in _instances) - { - if (instance != this) - { - instance._isDark = _isDark; - instance.StateHasChanged(); - } - } - StateHasChanged(); } catch @@ -85,7 +90,10 @@ public async ValueTask DisposeAsync() { - _instances.Remove(this); - await ValueTask.CompletedTask; + if (_observing) + { + try { await JSRuntime.InvokeVoidAsync("ShellUI.unobserveTheme", _handle); } catch { } + } + _selfRef?.Dispose(); } } diff --git a/src/ShellUI.Components/wwwroot/shellui.js b/src/ShellUI.Components/wwwroot/shellui.js index 032c768..53fcaca 100644 --- a/src/ShellUI.Components/wwwroot/shellui.js +++ b/src/ShellUI.Components/wwwroot/shellui.js @@ -20,6 +20,25 @@ Object.assign(window.ShellUI, { document.documentElement.classList.remove(className); }, + _themeObservers: new Map(), + observeTheme: function (handle, dotNetRef) { + this.unobserveTheme(handle); + const root = document.documentElement; + const observer = new MutationObserver(() => { + dotNetRef.invokeMethodAsync("OnThemeChanged", root.classList.contains("dark")).catch(() => {}); + }); + observer.observe(root, { attributes: true, attributeFilter: ["class"] }); + this._themeObservers.set(handle, observer); + return root.classList.contains("dark"); + }, + unobserveTheme: function (handle) { + const observer = this._themeObservers.get(handle); + if (observer) { + observer.disconnect(); + this._themeObservers.delete(handle); + } + }, + setupFileDrop: function (dropZoneId, inputElementId) { const dropZone = document.getElementById(dropZoneId); const input = document.getElementById(inputElementId); @@ -102,6 +121,8 @@ export function copyToClipboard(text) { return window.ShellUI.copyToClipboard(te export function focusElement(elementId) { return window.ShellUI.focusElement(elementId); } export function addClassToDocument(className) { return window.ShellUI.addClassToDocument(className); } export function removeClassFromDocument(className) { return window.ShellUI.removeClassFromDocument(className); } +export function observeTheme(handle, dotNetRef) { return window.ShellUI.observeTheme(handle, dotNetRef); } +export function unobserveTheme(handle) { return window.ShellUI.unobserveTheme(handle); } export function setupFileDrop(dropZoneId, inputElementId) { return window.ShellUI.setupFileDrop(dropZoneId, inputElementId); } export function registerShortcut(handle, key, ctrl, meta, shift, alt, dotNetRef) { return window.ShellUI.registerShortcut(handle, key, ctrl, meta, shift, alt, dotNetRef); diff --git a/src/ShellUI.Templates/Templates/ShellUIJsTemplate.cs b/src/ShellUI.Templates/Templates/ShellUIJsTemplate.cs index 6cbe316..bc2bce7 100644 --- a/src/ShellUI.Templates/Templates/ShellUIJsTemplate.cs +++ b/src/ShellUI.Templates/Templates/ShellUIJsTemplate.cs @@ -36,6 +36,26 @@ public static class ShellUIJsTemplate removeClassFromDocument: function (className) { document.documentElement.classList.remove(className); }, + + _themeObservers: new Map(), + observeTheme: function (handle, dotNetRef) { + this.unobserveTheme(handle); + const root = document.documentElement; + const observer = new MutationObserver(() => { + dotNetRef.invokeMethodAsync("OnThemeChanged", root.classList.contains("dark")).catch(() => {}); + }); + observer.observe(root, { attributes: true, attributeFilter: ["class"] }); + this._themeObservers.set(handle, observer); + return root.classList.contains("dark"); + }, + + unobserveTheme: function (handle) { + const observer = this._themeObservers.get(handle); + if (observer) { + observer.disconnect(); + this._themeObservers.delete(handle); + } + }, toggleClassOnDocument: function (className) { document.documentElement.classList.toggle(className); diff --git a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs index 776673f..4c859d9 100644 --- a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs +++ b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs @@ -42,7 +42,9 @@ @implements IAsyncDisposable @code { - private static readonly List _instances = new(); + private readonly string _handle = Guid.NewGuid().ToString(""N""); + private DotNetObjectReference? _selfRef; + private bool _observing; private bool _isDark = true; [Parameter] public string Size { get; set; } = ""default""; @@ -51,25 +53,38 @@ @implements IAsyncDisposable [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } - protected override void OnInitialized() - { - _instances.Add(this); - } - // JSRuntime is unavailable during prerender. protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; try { - var theme = await JSRuntime.InvokeAsync(""localStorage.getItem"", ""theme""); - _isDark = string.IsNullOrEmpty(theme) ? true : theme == ""dark""; - StateHasChanged(); + // Reports the page's current theme and keeps every toggle in sync, whoever changes it. + _selfRef = DotNetObjectReference.Create(this); + _isDark = await JSRuntime.InvokeAsync(""ShellUI.observeTheme"", _handle, _selfRef); + _observing = true; } catch { - _isDark = true; + try + { + var theme = await JSRuntime.InvokeAsync(""localStorage.getItem"", ""theme""); + _isDark = string.IsNullOrEmpty(theme) || theme == ""dark""; + } + catch + { + return; + } } + StateHasChanged(); + } + + [JSInvokable] + public Task OnThemeChanged(bool isDark) + { + if (_isDark == isDark) return Task.CompletedTask; + _isDark = isDark; + return InvokeAsync(StateHasChanged); } private async Task ToggleTheme() @@ -83,16 +98,6 @@ private async Task ToggleTheme() await JSRuntime.InvokeVoidAsync( _isDark ? ""ShellUI.addClassToDocument"" : ""ShellUI.removeClassFromDocument"", ""dark""); - - foreach (var instance in _instances) - { - if (instance != this) - { - instance._isDark = _isDark; - instance.StateHasChanged(); - } - } - StateHasChanged(); } catch @@ -103,8 +108,11 @@ await JSRuntime.InvokeVoidAsync( public async ValueTask DisposeAsync() { - _instances.Remove(this); - await ValueTask.CompletedTask; + if (_observing) + { + try { await JSRuntime.InvokeVoidAsync(""ShellUI.unobserveTheme"", _handle); } catch { } + } + _selfRef?.Dispose(); } } "; From cd6ecfbc45c7759dcfc5b407c7de2cf480621b72 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 2 Oct 2026 23:50:46 +0200 Subject: [PATCH 4/7] fix: DatePicker popover width and left-aligned DataPicker/MultiSelect options --- src/ShellUI.Components/Components/DataPicker.razor | 2 +- src/ShellUI.Components/Components/DatePicker.razor | 2 +- src/ShellUI.Components/Components/DateRangePicker.razor | 2 +- src/ShellUI.Components/Components/MultiSelect.razor | 2 +- src/ShellUI.Templates/Templates/DataPickerTemplate.cs | 2 +- src/ShellUI.Templates/Templates/DatePickerTemplate.cs | 2 +- src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs | 2 +- src/ShellUI.Templates/Templates/MultiSelectTemplate.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ShellUI.Components/Components/DataPicker.razor b/src/ShellUI.Components/Components/DataPicker.razor index e2f597b..2b6ae44 100644 --- a/src/ShellUI.Components/Components/DataPicker.razor +++ b/src/ShellUI.Components/Components/DataPicker.razor @@ -63,7 +63,7 @@