diff --git a/.github/workflows/preview-pages.yml b/.github/workflows/preview-pages.yml deleted file mode 100644 index decd86b..0000000 --- a/.github/workflows/preview-pages.yml +++ /dev/null @@ -1,65 +0,0 @@ -# Deploy ShellUI Preview to GitHub Pages (for docs embedding) -name: Deploy Preview to GitHub Pages - -on: - push: - branches: [ main ] - paths: - - 'NET10/ShellUI.Preview/**' - - 'src/ShellUI.Components/**' - - '.github/workflows/preview-pages.yml' - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Publish Preview (Blazor WASM) - run: | - dotnet publish NET10/ShellUI.Preview/ShellUI.Preview.csproj \ - -c Release \ - -o ./preview-publish - env: - # For GH Pages: base href = /repo-name/ (set your repo name) - # Leave empty for root: https://user.github.io/repo-name/ - BASE_HREF: / - - - name: Setup Pages - uses: actions/configure-pages@v4 - with: - # Auto-enable Pages on first run so this workflow can deploy without - # someone clicking through Settings → Pages first. Requires the - # GITHUB_TOKEN to have `pages: write` (already set above). - enablement: true - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./preview-publish/wwwroot - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor b/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor index 8fb28d7..cb13e82 100644 --- a/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor +++ b/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor @@ -124,7 +124,7 @@

Drawer & Sheet

- + @@ -134,7 +134,7 @@ - + diff --git a/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs b/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs index 218233e..a7040d8 100644 --- a/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs +++ b/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs @@ -24,3 +24,20 @@ public enum SortDirection Ascending, Descending } + +// Passed to OnDataRequest when DataTable is in ServerSide mode. The server-side data +// loader receives filter / sort / page state and returns just the current page. +public class DataTableRequest +{ + public int Skip { get; set; } + public int Take { get; set; } + public string? SortBy { get; set; } + public SortDirection SortDirection { get; set; } = SortDirection.None; + public string SearchQuery { get; set; } = ""; +} + +public class DataTableResponse +{ + public IEnumerable Items { get; set; } = Array.Empty(); + public int TotalCount { get; set; } +} diff --git a/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs b/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs index de05d84..08b0a1f 100644 --- a/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs +++ b/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs @@ -6,13 +6,20 @@ public class SonnerToastItem public string Message { get; set; } = ""; public string? Description { get; set; } public string Variant { get; set; } = "default"; + // Zero or negative disables auto-dismiss (toast stays until manually closed). + public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(4); public DateTime CreatedAt { get; } = DateTime.UtcNow; } public interface ISonnerService { IReadOnlyList Toasts { get; } - void Show(string message, string? description = null, string variant = "default"); + void Show(string message, string? description = null, string variant = "default", TimeSpan? duration = null); + void Success(string message, string? description = null, TimeSpan? duration = null); + void Error(string message, string? description = null, TimeSpan? duration = null); + void Info(string message, string? description = null, TimeSpan? duration = null); + void Warning(string message, string? description = null, TimeSpan? duration = null); + void Remove(string id); } public class SonnerService : ISonnerService @@ -21,15 +28,49 @@ public class SonnerService : ISonnerService public IReadOnlyList Toasts => _toasts; public event Action? OnChange; - public void Show(string message, string? description = null, string variant = "default") + public void Show(string message, string? description = null, string variant = "default", TimeSpan? duration = null) { - _toasts.Add(new SonnerToastItem { Message = message, Description = description, Variant = variant }); + var toast = new SonnerToastItem + { + Message = message, + Description = description, + Variant = variant, + Duration = duration ?? TimeSpan.FromSeconds(4) + }; + _toasts.Add(toast); OnChange?.Invoke(); + + if (toast.Duration > TimeSpan.Zero) + { + _ = AutoDismissAsync(toast.Id, toast.Duration); + } } + public void Success(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "success", duration); + + public void Error(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "destructive", duration); + + public void Info(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "info", duration); + + public void Warning(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "warning", duration); + public void Remove(string id) { - _toasts.RemoveAll(t => t.Id == id); - OnChange?.Invoke(); + var removed = _toasts.RemoveAll(t => t.Id == id); + if (removed > 0) OnChange?.Invoke(); + } + + private async Task AutoDismissAsync(string id, TimeSpan delay) + { + try + { + await Task.Delay(delay); + Remove(id); + } + catch { } } } diff --git a/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor b/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor index 010736f..eeb75df 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor @@ -1,4 +1,7 @@ @namespace BlazorInteractiveServer.Components.UI +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS
} - +
@@ -73,7 +75,7 @@ @if (_isOpen) { -
+
} @code { @@ -84,6 +86,9 @@ [Parameter] public string Placeholder { get; set; } = "Pick a date range"; [Parameter] public bool AllowClear { get; set; } = true; [Parameter] public bool Disabled { get; set; } + /// Dismiss the calendar when the page scrolls. Matches shadcn/Radix behavior. + /// Set false to keep the calendar open across scroll. + [Parameter] public bool CloseOnScroll { get; set; } [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } @@ -91,6 +96,9 @@ private bool _isOpen; private DateTime _currentMonth = DateTime.Now; private bool _selectingStart = true; + private DotNetObjectReference? _selfRef; + private readonly string _dismissHandle = Guid.NewGuid().ToString("N"); + private bool _dismissRegistered; private string GetDisplayText() { @@ -128,7 +136,43 @@ return baseClass; } - private void ToggleCalendar() => _isOpen = !_isOpen; + private async Task ToggleCalendar() + { + _isOpen = !_isOpen; + if (_isOpen) await RegisterDismissAsync(); + else await UnregisterDismissAsync(); + } + + [JSInvokable] + public async Task OnDismissEvent() + { + if (_isOpen) + { + _isOpen = false; + await UnregisterDismissAsync(); + StateHasChanged(); + } + } + + private async Task RegisterDismissAsync() + { + if (!CloseOnScroll || _dismissRegistered) return; + _selfRef ??= DotNetObjectReference.Create(this); + try + { + await JS.InvokeVoidAsync("ShellUI.onDismissEvents", _dismissHandle, _selfRef); + _dismissRegistered = true; + } + catch { } + } + + private async Task UnregisterDismissAsync() + { + if (!_dismissRegistered) return; + try { await JS.InvokeVoidAsync("ShellUI.offDismissEvents", _dismissHandle); } catch { } + _dismissRegistered = false; + } + private void PreviousMonth() => _currentMonth = _currentMonth.AddMonths(-1); private void NextMonth() => _currentMonth = _currentMonth.AddMonths(1); @@ -176,11 +220,22 @@ var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); var lastDay = firstDay.AddMonths(1).AddDays(-1); var startDayOfWeek = (int)firstDay.DayOfWeek; - + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); - + return days; } -} + private async Task CloseBackdrop() + { + _isOpen = false; + await UnregisterDismissAsync(); + } + + public async ValueTask DisposeAsync() + { + await UnregisterDismissAsync(); + _selfRef?.Dispose(); + } +} diff --git a/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor b/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor index 6eb2fc1..ca643c5 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor @@ -1,5 +1,7 @@ @namespace BlazorInteractiveServer.Components.UI -@using BlazorInteractiveServer.Components +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS @if (Dialog?.Open == true) { @@ -20,6 +22,8 @@ [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + private bool _isLocked; + private async Task Close() { if (Dialog != null) @@ -27,6 +31,25 @@ await Dialog.SetOpen(false); } } -} + protected override async Task OnAfterRenderAsync(bool firstRender) + { + var shouldBeLocked = Dialog?.Open == true; + if (shouldBeLocked && !_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.lockBodyScroll"); _isLocked = true; } catch { } + } + else if (!shouldBeLocked && _isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); _isLocked = false; } catch { } + } + } + public async ValueTask DisposeAsync() + { + if (_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); } catch { } + } + } +} diff --git a/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor b/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor index ca38146..206fae0 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor @@ -1,5 +1,7 @@ @namespace BlazorInteractiveServer.Components.UI -@using BlazorInteractiveServer.Components +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS @if (Parent?.Open == true) { @@ -19,8 +21,31 @@ [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + private bool _isLocked; + private async Task Close() { if (Parent != null) await Parent.SetOpen(false); } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + var shouldBeLocked = Parent?.Open == true; + if (shouldBeLocked && !_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.lockBodyScroll"); _isLocked = true; } catch { } + } + else if (!shouldBeLocked && _isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); _isLocked = false; } catch { } + } + } + + public async ValueTask DisposeAsync() + { + if (_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); } catch { } + } + } } diff --git a/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor b/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor index 8750345..300c04b 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor @@ -1,6 +1,9 @@ @namespace BlazorInteractiveServer.Components.UI +@using Microsoft.JSInterop @typeparam TItem @typeparam TKey where TKey : notnull +@implements IAsyncDisposable +@inject IJSRuntime JS
- - - - Dialog Title - Preview for docs embedding. - -

Content goes here.

- - - - - - - - -
- - -@code { - private bool _open; -} diff --git a/NET10/ShellUI.Preview/Layout/MainLayout.razor b/NET10/ShellUI.Preview/Layout/MainLayout.razor deleted file mode 100644 index 41ebcea..0000000 --- a/NET10/ShellUI.Preview/Layout/MainLayout.razor +++ /dev/null @@ -1,14 +0,0 @@ -@inherits LayoutComponentBase -
- -
- @Body -
-
diff --git a/NET10/ShellUI.Preview/Layout/PreviewLayout.razor b/NET10/ShellUI.Preview/Layout/PreviewLayout.razor deleted file mode 100644 index b2b5c2d..0000000 --- a/NET10/ShellUI.Preview/Layout/PreviewLayout.razor +++ /dev/null @@ -1,11 +0,0 @@ -@inherits LayoutComponentBase - -
-
- ← Components -
-
- @Body -
-
- diff --git a/NET10/ShellUI.Preview/Pages/ComponentPreview.razor b/NET10/ShellUI.Preview/Pages/ComponentPreview.razor deleted file mode 100644 index 6ee4409..0000000 --- a/NET10/ShellUI.Preview/Pages/ComponentPreview.razor +++ /dev/null @@ -1,249 +0,0 @@ -@page "/preview/{ComponentName}" -@layout Layout.PreviewLayout -@using ShellUI.Components -@using ShellUI.Components.Models -@using ShellUI.Preview.Components - -
- @switch (_id) - { - case "button": - - break; - case "badge": - Badge - break; - case "switch": - - break; - case "checkbox": - - break; - case "input": - - break; - case "textarea": -