From 22333c06e6ee67de95261fb175142651dd941091 Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 20 Oct 2025 00:21:38 +0200 Subject: [PATCH 01/38] feat: Integrate Google Fonts and enhance ThemeToggle component with customizable class parameter for improved styling flexibility --- .../Components/App.razor | 7 +- .../Components/Layout/MainLayout.razor | 8 +- .../Components/UI/ThemeToggle.razor | 11 +- NET9/BlazorInteractiveServer/wwwroot/app.css | 164 +++++++++++----- .../BlazorInteractiveServer/wwwroot/input.css | 183 +++++++++++++----- 5 files changed, 270 insertions(+), 103 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/App.razor b/NET9/BlazorInteractiveServer/Components/App.razor index 4330148..9a75946 100644 --- a/NET9/BlazorInteractiveServer/Components/App.razor +++ b/NET9/BlazorInteractiveServer/Components/App.razor @@ -5,7 +5,12 @@ - + + + + + + diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index 2fe1afc..7d48164 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -46,9 +46,7 @@ - + Menu
- + diff --git a/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor index 3ed11fa..a523e5b 100644 --- a/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor +++ b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor @@ -2,9 +2,11 @@ @inject IJSRuntime JSRuntime @implements IAsyncDisposable -
+ +
+ + +

+ @_currentMonth.ToString("MMMM yyyy", CultureInfo.CurrentCulture) +

+ + +
+ + +
+ @foreach (var day in new[] { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" }) + { +
@day
+ } +
+ + +
+ @for (int i = 0; i < _leadingDays; i++) + { +
+ } + + @for (int day = 1; day <= _daysInMonth; day++) + { + var date = new DateTime(_currentMonth.Year, _currentMonth.Month, day); + var isSelected = SelectedDate.HasValue && date.Date == SelectedDate.Value.Date; + var isToday = date.Date == DateTime.Today; + var isDisabled = (MinDate.HasValue && date < MinDate.Value) || (MaxDate.HasValue && date > MaxDate.Value); + + + } +
+
+ +@code { + [Parameter] + public DateTime? SelectedDate { get; set; } + + [Parameter] + public DateTime? MinDate { get; set; } + + [Parameter] + public DateTime? MaxDate { get; set; } + + [Parameter] + public EventCallback SelectedDateChanged { get; set; } + + private DateTime _currentMonth = DateTime.Today; + private int _daysInMonth; + private int _leadingDays; + + protected override void OnInitialized() + { + UpdateCalendar(); + } + + protected override void OnParametersSet() + { + if (SelectedDate.HasValue) + { + _currentMonth = SelectedDate.Value; + UpdateCalendar(); + } + } + + private void UpdateCalendar() + { + _daysInMonth = DateTime.DaysInMonth(_currentMonth.Year, _currentMonth.Month); + var firstDayOfMonth = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); + _leadingDays = (int)firstDayOfMonth.DayOfWeek; + } + + private void PreviousMonth() + { + _currentMonth = _currentMonth.AddMonths(-1); + UpdateCalendar(); + } + + private void NextMonth() + { + _currentMonth = _currentMonth.AddMonths(1); + UpdateCalendar(); + } + + private async Task SelectDate(DateTime date) + { + SelectedDate = date; + await SelectedDateChanged.InvokeAsync(date); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Command.razor b/NET9/BlazorInteractiveServer/Components/UI/Command.razor new file mode 100644 index 0000000..9607cf9 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Command.razor @@ -0,0 +1,159 @@ +@using Microsoft.JSInterop +@using BlazorInteractiveServer.Components.Models +@using BlazorInteractiveServer.Components.UI + + + + +
+ + + + +
+ + +
+ @if (_filteredCommands.Any()) + { + @foreach (var command in _filteredCommands) + { + + } + } + else + { +
+ No results found. +
+ } +
+
+
+ +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Type a command or search..."; + + [Parameter] + public List Commands { get; set; } = new(); + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public EventCallback CommandSelected { get; set; } + + private string _searchQuery = ""; + private List _filteredCommands = new(); + private ElementReference _searchInput; + private bool _shouldFocus; + + protected override void OnParametersSet() + { + UpdateFilteredCommands(); + if (IsOpen) + { + _shouldFocus = true; + } + } + + private void UpdateFilteredCommands() + { + if (string.IsNullOrWhiteSpace(_searchQuery)) + { + _filteredCommands = Commands.ToList(); + } + else + { + _filteredCommands = Commands + .Where(c => + c.Title.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) || + (c.Description?.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) == true)) + .ToList(); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (_shouldFocus) + { + _shouldFocus = false; + try + { + await _searchInput.FocusAsync(); + } + catch { } + } + } + + private async Task SelectCommand(CommandItem command) + { + await CommandSelected.InvokeAsync(command); + await CloseCommand(); + } + + private async Task CloseCommand() + { + IsOpen = false; + _searchQuery = ""; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private async Task OnKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Escape") + { + await CloseCommand(); + } + else if (e.Key == "Enter" && _filteredCommands.Any()) + { + await SelectCommand(_filteredCommands.First()); + } + else if (e.Key == "ArrowDown" && _filteredCommands.Count > 1) + { + // Could implement keyboard navigation here + } + else if (e.Key == "ArrowUp" && _filteredCommands.Count > 1) + { + // Could implement keyboard navigation here + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/DataTable.razor b/NET9/BlazorInteractiveServer/Components/UI/DataTable.razor new file mode 100644 index 0000000..2ff5493 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/DataTable.razor @@ -0,0 +1,353 @@ +@typeparam TItem +@using System.Linq.Dynamic.Core +@using BlazorInteractiveServer.Components.Models + +
+ + @if (ShowFilters) + { +
+ +
+ } + + +
+ + + + @if (ShowSelection) + { + + } + @foreach (var column in Columns) + { + + } + @if (RowActions?.Any() == true) + { + + } + + + + @if (_filteredItems.Any()) + { + @foreach (var item in _paginatedItems) + { + + @if (ShowSelection) + { + + } + @foreach (var column in Columns) + { + + } + @if (RowActions?.Any() == true) + { + + } + + } + } + else + { + + + + } + +
+ + +
+ @column.Header + @if (column.Sortable) + { + + + + } +
+
Actions
+ + @column.CellTemplate(item) +
+ + @if (_openActionsItem?.Equals(item) == true) + { +
+ @foreach (var action in RowActions) + { + + } +
+ } +
+
+ No results found. +
+
+ + + @if (ShowPagination && _filteredItems.Any()) + { +
+
+ @if (_selectedItems.Any()) + { + @($"{_selectedItems.Count} item(s) selected") + } + else + { + @($"Showing {_startIndex} - {_endIndex} of {_filteredItems.Count()} results") + } +
+
+
+

Rows per page

+
+ +
+ + + +
+
+
+
+ Page @_currentPage of @_totalPages +
+
+ + +
+
+
+ } +
+ +@code { + [Parameter] + public IEnumerable Items { get; set; } = new List(); + + [Parameter] + public List> Columns { get; set; } = new(); + + [Parameter] + public List>? RowActions { get; set; } + + [Parameter] + public bool ShowFilters { get; set; } = true; + + [Parameter] + public bool ShowPagination { get; set; } = true; + + [Parameter] + public bool ShowSelection { get; set; } = false; + + [Parameter] + public int PageSize { get; set; } = 10; + + [Parameter] + public EventCallback> SelectedItemsChanged { get; set; } + + private string _filterText = ""; + private IEnumerable _filteredItems = new List(); + private IEnumerable _paginatedItems = new List(); + private HashSet _selectedItems = new(); + private int _currentPage = 1; + private int _totalPages = 1; + private int _startIndex = 1; + private int _endIndex = 1; + private int _pageSize = 10; + private TItem? _openActionsItem = default(TItem); + + protected override void OnParametersSet() + { + UpdateFilteredItems(); + } + + private void UpdateFilteredItems() + { + // Apply filtering + _filteredItems = string.IsNullOrWhiteSpace(_filterText) + ? Items + : Items.Where(item => + Columns.Any(col => + { + // Simple text search - check if any property contains the filter text + var property = typeof(TItem).GetProperty(col.PropertyName); + if (property != null) + { + var value = property.GetValue(item)?.ToString() ?? ""; + return value.Contains(_filterText, StringComparison.OrdinalIgnoreCase); + } + return false; + })); + + // Apply sorting + var sortColumn = Columns.FirstOrDefault(c => c.SortDirection != SortDirection.None); + if (sortColumn != null) + { + var sortDirection = sortColumn.SortDirection == SortDirection.Ascending ? "" : "descending"; + _filteredItems = _filteredItems.AsQueryable().OrderBy($"{sortColumn.PropertyName} {sortDirection}"); + } + + // Update pagination + _totalPages = (int)Math.Ceiling(_filteredItems.Count() / (double)_pageSize); + _currentPage = Math.Min(_currentPage, Math.Max(1, _totalPages)); + _startIndex = (_currentPage - 1) * _pageSize + 1; + _endIndex = Math.Min(_currentPage * _pageSize, _filteredItems.Count()); + _paginatedItems = _filteredItems.Skip((_currentPage - 1) * _pageSize).Take(_pageSize); + + StateHasChanged(); + } + + private void OnFilterInput(ChangeEventArgs e) + { + _filterText = e.Value?.ToString() ?? ""; + _currentPage = 1; // Reset to first page + UpdateFilteredItems(); + } + + private void OnFilterChanged(string value) + { + _filterText = value; + _currentPage = 1; // Reset to first page + UpdateFilteredItems(); + } + + private void OnSortClicked(DataTableColumn column) + { + if (!column.Sortable) return; + + // Reset other columns + foreach (var col in Columns.Where(c => c != column)) + { + col.SortDirection = SortDirection.None; + } + + // Cycle sort direction + column.SortDirection = column.SortDirection switch + { + SortDirection.None => SortDirection.Ascending, + SortDirection.Ascending => SortDirection.Descending, + SortDirection.Descending => SortDirection.None, + _ => SortDirection.Ascending + }; + + UpdateFilteredItems(); + } + + private void OnSelectAllChanged(bool selected) + { + if (selected) + { + _selectedItems = new HashSet(_filteredItems); + } + else + { + _selectedItems.Clear(); + } + + SelectedItemsChanged.InvokeAsync(_selectedItems); + StateHasChanged(); + } + + private void OnItemSelectionChanged(TItem item, bool selected) + { + if (selected) + { + _selectedItems.Add(item); + } + else + { + _selectedItems.Remove(item); + } + + SelectedItemsChanged.InvokeAsync(_selectedItems); + StateHasChanged(); + } + + private bool IsSelected(TItem item) => _selectedItems.Contains(item); + + private void OnPageSizeChanged(ChangeEventArgs e) + { + if (int.TryParse(e.Value?.ToString(), out var size)) + { + _pageSize = size; + _currentPage = 1; + UpdateFilteredItems(); + } + } + + private void OnPreviousPage() + { + if (_currentPage > 1) + { + _currentPage--; + UpdateFilteredItems(); + } + } + + private void OnNextPage() + { + if (_currentPage < _totalPages) + { + _currentPage++; + UpdateFilteredItems(); + } + } + + private void ToggleActionsDropdown(TItem item) + { + _openActionsItem = _openActionsItem?.Equals(item) == true ? default(TItem) : item; + StateHasChanged(); + } + + private void CloseActionsDropdown() + { + _openActionsItem = default(TItem); + StateHasChanged(); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/HoverCard.razor b/NET9/BlazorInteractiveServer/Components/UI/HoverCard.razor new file mode 100644 index 0000000..acddf7f --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/HoverCard.razor @@ -0,0 +1,91 @@ +@using Microsoft.JSInterop + +
+
+ @ChildContent +
+ + @if (_isVisible) + { +
+ @CardContent +
+ } +
+ +@code { + [Parameter] + public RenderFragment ChildContent { get; set; } = null!; + + [Parameter] + public RenderFragment CardContent { get; set; } = null!; + + [Parameter] + public string Side { get; set; } = "bottom"; // top, right, bottom, left + + [Parameter] + public string Align { get; set; } = "center"; // start, center, end + + [Parameter] + public string Class { get; set; } = ""; + + private bool _isVisible = false; + private System.Timers.Timer? _timer; + + private void ShowCard() + { + _timer?.Stop(); + _isVisible = true; + StateHasChanged(); + } + + private void HideCard() + { + _timer?.Stop(); + _timer = new System.Timers.Timer(150); // 150ms delay + _timer.Elapsed += (sender, e) => + { + _isVisible = false; + InvokeAsync(StateHasChanged); + _timer?.Dispose(); + _timer = null; + }; + _timer.Start(); + } + + private string _getPositionClasses() + { + var classes = new List(); + + switch (Side) + { + case "top": + classes.Add("bottom-full mb-2"); + break; + case "right": + classes.Add("left-full ml-2"); + break; + case "bottom": + classes.Add("top-full mt-2"); + break; + case "left": + classes.Add("right-full mr-2"); + break; + } + + switch (Align) + { + case "start": + classes.Add(Side == "top" || Side == "bottom" ? "left-0" : "top-0"); + break; + case "center": + classes.Add(Side == "top" || Side == "bottom" ? "left-1/2 -translate-x-1/2" : "top-1/2 -translate-y-1/2"); + break; + case "end": + classes.Add(Side == "top" || Side == "bottom" ? "right-0" : "bottom-0"); + break; + } + + return string.Join(" ", classes); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Loading.razor b/NET9/BlazorInteractiveServer/Components/UI/Loading.razor new file mode 100644 index 0000000..34c526c --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Loading.razor @@ -0,0 +1,35 @@ +@if (Variant == "spinner") +{ +
+} +else if (Variant == "dots") +{ +
+
+
+
+
+} +else if (Variant == "pulse") +{ +
+} +else if (Variant == "ring") +{ +
+
+
+
+} + + +@code { + [Parameter] + public string Variant { get; set; } = "spinner"; // spinner, dots, pulse, ring + + [Parameter] + public string Size { get; set; } = "default"; // sm, default, lg + + [Parameter] + public string Class { get; set; } = ""; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogTemplate.cs new file mode 100644 index 0000000..f832909 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogTemplate.cs @@ -0,0 +1,91 @@ +namespace ShellUI.Templates.Templates; + +public static class AlertDialogTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog", + Description = "Modal dialog for confirmations and alerts", + Category = ComponentCategory.Overlay, + Dependencies = new[] { "button" } + }; + + public static string Content => @" +@using Microsoft.JSInterop + +
+
+
+ @if (!string.IsNullOrEmpty(Title)) + { +
+

@Title

+ @if (!string.IsNullOrEmpty(Description)) + { +

@Description

+ } +
+ } + +
+ @if (CancelText != null) + { + + } + +
+
+
+ +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public string Title { get; set; } = """"; + + [Parameter] + public string Description { get; set; } = """"; + + [Parameter] + public string ConfirmText { get; set; } = ""Continue""; + + [Parameter] + public string? CancelText { get; set; } = ""Cancel""; + + [Parameter] + public string ConfirmVariant { get; set; } = ""default""; + + [Parameter] + public EventCallback OnConfirm { get; set; } + + [Parameter] + public EventCallback OnCancel { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + private async Task OnConfirm() + { + await OnConfirm.InvokeAsync(); + await CloseDialog(); + } + + private async Task OnCancel() + { + await OnCancel.InvokeAsync(); + await CloseDialog(); + } + + private async Task OnBackdropClick() + { + await CloseDialog(); + } + + private async Task CloseDialog() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(IsOpen); + } +}"; +} diff --git a/src/ShellUI.Templates/Templates/CalendarTemplate.cs b/src/ShellUI.Templates/Templates/CalendarTemplate.cs new file mode 100644 index 0000000..2c7e444 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CalendarTemplate.cs @@ -0,0 +1,128 @@ +namespace ShellUI.Templates.Templates; + +public static class CalendarTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "calendar", + Description = "Full calendar component for date selection", + Category = ComponentCategory.DataDisplay, + Dependencies = new string[] { } + }; + + public static string Content => @" +@using System.Globalization + +
+ +
+ + +

+ @_currentMonth.ToString(""MMMM yyyy"", CultureInfo.CurrentCulture) +

+ + +
+ + +
+ @foreach (var day in new[] { ""Su"", ""Mo"", ""Tu"", ""We"", ""Th"", ""Fr"", ""Sa"" }) + { +
@day
+ } +
+ + +
+ @for (int i = 0; i < _leadingDays; i++) + { +
+ } + + @for (int day = 1; day <= _daysInMonth; day++) + { + var date = new DateTime(_currentMonth.Year, _currentMonth.Month, day); + var isSelected = SelectedDate.HasValue && date.Date == SelectedDate.Value.Date; + var isToday = date.Date == DateTime.Today; + var isDisabled = (MinDate.HasValue && date < MinDate.Value) || (MaxDate.HasValue && date > MaxDate.Value); + + + } +
+
+ +@code { + [Parameter] + public DateTime? SelectedDate { get; set; } + + [Parameter] + public DateTime? MinDate { get; set; } + + [Parameter] + public DateTime? MaxDate { get; set; } + + [Parameter] + public EventCallback SelectedDateChanged { get; set; } + + private DateTime _currentMonth = DateTime.Today; + private int _daysInMonth; + private int _leadingDays; + + protected override void OnInitialized() + { + UpdateCalendar(); + } + + protected override void OnParametersSet() + { + if (SelectedDate.HasValue) + { + _currentMonth = SelectedDate.Value; + UpdateCalendar(); + } + } + + private void UpdateCalendar() + { + _daysInMonth = DateTime.DaysInMonth(_currentMonth.Year, _currentMonth.Month); + var firstDayOfMonth = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); + _leadingDays = (int)firstDayOfMonth.DayOfWeek; + } + + private void PreviousMonth() + { + _currentMonth = _currentMonth.AddMonths(-1); + UpdateCalendar(); + } + + private void NextMonth() + { + _currentMonth = _currentMonth.AddMonths(1); + UpdateCalendar(); + } + + private async Task SelectDate(DateTime date) + { + SelectedDate = date; + await SelectedDateChanged.InvokeAsync(date); + } +}"; +} diff --git a/src/ShellUI.Templates/Templates/CommandTemplate.cs b/src/ShellUI.Templates/Templates/CommandTemplate.cs new file mode 100644 index 0000000..994aa84 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CommandTemplate.cs @@ -0,0 +1,164 @@ +namespace ShellUI.Templates.Templates; + +public static class CommandTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "command", + Description = "Command palette component for quick actions", + Category = ComponentCategory.Overlay, + Dependencies = new string[] { } + }; + + public static string Content => @" +@using Microsoft.JSInterop + +@if (IsOpen) +{ +
+
+ +
+ + + + +
+ + +
+ @if (_filteredCommands.Any()) + { + @foreach (var command in _filteredCommands) + { + + } + } + else + { +
+ No results found. +
+ } +
+
+} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public string Placeholder { get; set; } = ""Type a command or search...""; + + [Parameter] + public List Commands { get; set; } = new(); + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public EventCallback CommandSelected { get; set; } + + private string _searchQuery = """"; + private List _filteredCommands = new(); + private ElementReference _searchInput; + + protected override void OnParametersSet() + { + UpdateFilteredCommands(); + } + + private void UpdateFilteredCommands() + { + if (string.IsNullOrWhiteSpace(_searchQuery)) + { + _filteredCommands = Commands.ToList(); + } + else + { + _filteredCommands = Commands + .Where(c => + c.Title.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) || + (c.Description?.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) == true)) + .ToList(); + } + } + + private async Task SelectCommand(CommandItem command) + { + await CommandSelected.InvokeAsync(command); + await CloseCommand(); + } + + private async Task CloseCommand() + { + IsOpen = false; + _searchQuery = """"; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private void OnKeyDown(KeyboardEventArgs e) + { + if (e.Key == ""Escape"") + { + CloseCommand(); + } + else if (e.Key == ""Enter"" && _filteredCommands.Any()) + { + SelectCommand(_filteredCommands.First()); + } + else if (e.Key == ""ArrowDown"" && _filteredCommands.Count > 1) + { + // Could implement keyboard navigation here + } + else if (e.Key == ""ArrowUp"" && _filteredCommands.Count > 1) + { + // Could implement keyboard navigation here + } + } +} + +public class CommandItem +{ + public string Title { get; set; } = """"; + public string? Description { get; set; } + public string? Icon { get; set; } + public string? Shortcut { get; set; } + public Action? Action { get; set; } +}"; +} diff --git a/src/ShellUI.Templates/Templates/DataTableTemplate.cs b/src/ShellUI.Templates/Templates/DataTableTemplate.cs new file mode 100644 index 0000000..f480cba --- /dev/null +++ b/src/ShellUI.Templates/Templates/DataTableTemplate.cs @@ -0,0 +1,353 @@ +namespace ShellUI.Templates.Templates; + +public static class DataTableTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "data-table", + Description = "Advanced data table with sorting, filtering, pagination, and row selection", + Category = ComponentCategory.DataDisplay, + Dependencies = new[] { "table", "input", "checkbox", "select", "button", "dropdown" } + }; + + public static string Content => @" +@typeparam TItem +@using System.Linq.Dynamic.Core + +
+ + @if (ShowFilters) + { +
+ +
+ } + + +
+ + + + @if (ShowSelection) + { + + + + } + @foreach (var column in Columns) + { + column.Sortable ? OnSortClicked(column) : Task.CompletedTask""> +
+ @column.Header + @if (column.Sortable) + { + + + + } +
+
+ } + @if (RowActions?.Any() == true) + { + Actions + } +
+
+ + @if (_filteredItems.Any()) + { + @foreach (var item in _paginatedItems) + { + + @if (ShowSelection) + { + + OnItemSelectionChanged(item, checked)"" /> + + } + @foreach (var column in Columns) + { + @column.CellTemplate(item) + } + @if (RowActions?.Any() == true) + { + + + + + + + @foreach (var action in RowActions) + { + + } + + + + } + + } + } + else + { + + + No results found. + + + } + +
+
+ + + @if (ShowPagination && _filteredItems.Any()) + { +
+
+ @if (_selectedItems.Any()) + { + @_selectedItems.Count item(s) selected + } + else + { + Showing @_startIndex - @_endIndex of @_filteredItems.Count results + } +
+
+
+

Rows per page

+ +
+
+ Page @_currentPage of @_totalPages +
+
+ + +
+
+
+ } +
+ +@code { + [Parameter] + public IEnumerable Items { get; set; } = new List(); + + [Parameter] + public List> Columns { get; set; } = new(); + + [Parameter] + public List>? RowActions { get; set; } + + [Parameter] + public bool ShowFilters { get; set; } = true; + + [Parameter] + public bool ShowPagination { get; set; } = true; + + [Parameter] + public bool ShowSelection { get; set; } = false; + + [Parameter] + public int PageSize { get; set; } = 10; + + [Parameter] + public EventCallback> SelectedItemsChanged { get; set; } + + private string _filterText = """"; + private IEnumerable _filteredItems = new List(); + private IEnumerable _paginatedItems = new List(); + private HashSet _selectedItems = new(); + private int _currentPage = 1; + private int _totalPages = 1; + private int _startIndex = 1; + private int _endIndex = 1; + + protected override void OnParametersSet() + { + UpdateFilteredItems(); + } + + private void UpdateFilteredItems() + { + // Apply filtering + _filteredItems = string.IsNullOrWhiteSpace(_filterText) + ? Items + : Items.Where(item => + Columns.Any(col => col.FilterPredicate?.Invoke(item, _filterText) == true)); + + // Apply sorting + var sortColumn = Columns.FirstOrDefault(c => c.SortDirection != SortDirection.None); + if (sortColumn != null) + { + var sortDirection = sortColumn.SortDirection == SortDirection.Ascending ? """" : ""descending""; + _filteredItems = _filteredItems.AsQueryable().OrderBy($""{sortColumn.PropertyName} {sortDirection}""); + } + + // Update pagination + _totalPages = (int)Math.Ceiling(_filteredItems.Count() / (double)PageSize); + _currentPage = Math.Min(_currentPage, Math.Max(1, _totalPages)); + _startIndex = (_currentPage - 1) * PageSize + 1; + _endIndex = Math.Min(_currentPage * PageSize, _filteredItems.Count()); + _paginatedItems = _filteredItems.Skip((_currentPage - 1) * PageSize).Take(PageSize); + + StateHasChanged(); + } + + private void OnFilterChanged(string value) + { + _filterText = value; + _currentPage = 1; // Reset to first page + UpdateFilteredItems(); + } + + private void OnSortClicked(DataTableColumn column) + { + if (!column.Sortable) return; + + // Reset other columns + foreach (var col in Columns.Where(c => c != column)) + { + col.SortDirection = SortDirection.None; + } + + // Cycle sort direction + column.SortDirection = column.SortDirection switch + { + SortDirection.None => SortDirection.Ascending, + SortDirection.Ascending => SortDirection.Descending, + SortDirection.Descending => SortDirection.None, + _ => SortDirection.Ascending + }; + + UpdateFilteredItems(); + } + + private void OnSelectAllChanged(bool selected) + { + if (selected) + { + _selectedItems = new HashSet(_filteredItems); + } + else + { + _selectedItems.Clear(); + } + + SelectedItemsChanged.InvokeAsync(_selectedItems); + StateHasChanged(); + } + + private void OnItemSelectionChanged(TItem item, bool selected) + { + if (selected) + { + _selectedItems.Add(item); + } + else + { + _selectedItems.Remove(item); + } + + SelectedItemsChanged.InvokeAsync(_selectedItems); + StateHasChanged(); + } + + private bool IsSelected(TItem item) => _selectedItems.Contains(item); + + private void OnPageSizeChanged(string value) + { + if (int.TryParse(value, out var size)) + { + PageSize = size; + _currentPage = 1; + UpdateFilteredItems(); + } + } + + private void OnPreviousPage() + { + if (_currentPage > 1) + { + _currentPage--; + UpdateFilteredItems(); + } + } + + private void OnNextPage() + { + if (_currentPage < _totalPages) + { + _currentPage++; + UpdateFilteredItems(); + } + } +} + +public class DataTableColumn +{ + public string Header { get; set; } = """"; + public string PropertyName { get; set; } = """"; + public bool Sortable { get; set; } = true; + public SortDirection SortDirection { get; set; } = SortDirection.None; + public RenderFragment CellTemplate { get; set; } = null!; + public Func? FilterPredicate { get; set; } +} + +public class DataTableAction +{ + public string Label { get; set; } = """"; + public Action Action { get; set; } = null!; +} + +public enum SortDirection +{ + None, + Ascending, + Descending +}"; +} diff --git a/src/ShellUI.Templates/Templates/HoverCardTemplate.cs b/src/ShellUI.Templates/Templates/HoverCardTemplate.cs new file mode 100644 index 0000000..e78a159 --- /dev/null +++ b/src/ShellUI.Templates/Templates/HoverCardTemplate.cs @@ -0,0 +1,105 @@ +namespace ShellUI.Templates.Templates; + +public static class HoverCardTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "hover-card", + Description = "Rich hover content component", + Category = ComponentCategory.Overlay, + Dependencies = new string[] { } + }; + + public static string Content => @" +@using Microsoft.JSInterop + +
+
+ @ChildContent +
+ + @if (_isVisible) + { +
+ @CardContent +
+ } +
+ +@code { + [Parameter] + public RenderFragment ChildContent { get; set; } = null!; + + [Parameter] + public RenderFragment CardContent { get; set; } = null!; + + [Parameter] + public string Side { get; set; } = ""bottom""; // top, right, bottom, left + + [Parameter] + public string Align { get; set; } = ""center""; // start, center, end + + [Parameter] + public string Class { get; set; } = """"; + + private bool _isVisible = false; + private System.Timers.Timer? _timer; + + private void ShowCard() + { + _timer?.Stop(); + _isVisible = true; + StateHasChanged(); + } + + private void HideCard() + { + _timer?.Stop(); + _timer = new System.Timers.Timer(150); // 150ms delay + _timer.Elapsed += (sender, e) => + { + _isVisible = false; + InvokeAsync(StateHasChanged); + _timer?.Dispose(); + _timer = null; + }; + _timer.Start(); + } + + private string _getPositionClasses() + { + var classes = new List(); + + switch (Side) + { + case ""top"": + classes.Add(""bottom-full mb-2""); + break; + case ""right"": + classes.Add(""left-full ml-2""); + break; + case ""bottom"": + classes.Add(""top-full mt-2""); + break; + case ""left"": + classes.Add(""right-full mr-2""); + break; + } + + switch (Align) + { + case ""start"": + classes.Add(Side == ""top"" || Side == ""bottom"" ? ""left-0"" : ""top-0""); + break; + case ""center"": + classes.Add(Side == ""top"" || Side == ""bottom"" ? ""left-1/2 -translate-x-1/2"" : ""top-1/2 -translate-y-1/2""); + break; + case ""end"": + classes.Add(Side == ""top"" || Side == ""bottom"" ? ""right-0"" : ""bottom-0""); + break; + } + + return string.Join("" "", classes); + } +}"; +} diff --git a/src/ShellUI.Templates/Templates/LoadingTemplate.cs b/src/ShellUI.Templates/Templates/LoadingTemplate.cs new file mode 100644 index 0000000..bb9fcf6 --- /dev/null +++ b/src/ShellUI.Templates/Templates/LoadingTemplate.cs @@ -0,0 +1,48 @@ +namespace ShellUI.Templates.Templates; + +public static class LoadingTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "loading", + Description = "Loading spinner and skeleton components", + Category = ComponentCategory.Feedback, + Dependencies = new string[] { } + }; + + public static string Content => @" +@if (Variant == ""spinner"") +{ +
+} +else if (Variant == ""dots"") +{ +
+
+
+
+
+} +else if (Variant == ""pulse"") +{ +
+} +else if (Variant == ""ring"") +{ +
+
+
+
+} + +@code { + [Parameter] + public string Variant { get; set; } = ""spinner""; // spinner, dots, pulse, ring + + [Parameter] + public string Size { get; set; } = ""default""; // sm, default, lg + + [Parameter] + public string Class { get; set; } = """"; +}"; +} From 595c33b5531eeb2b4fc7bc5e6d352988fbe82073 Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 20 Oct 2025 21:48:43 +0200 Subject: [PATCH 09/38] feat: Implement command palette functionality with keyboard shortcuts and focus management for enhanced user interaction --- .../Components/Layout/MainLayout.razor | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index 7d48164..6a9ab68 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -1,6 +1,7 @@ @inherits LayoutComponentBase +@inject IJSRuntime JSRuntime -
+
@@ -32,8 +33,9 @@