From 635fd48c053b7dcfc6888859bb2ac26c5ebad235 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 11 Feb 2026 01:16:34 +0200 Subject: [PATCH 01/49] Implement AppSidebar and Sidebar components for enhanced navigation ### Summary - Introduced a new `AppSidebar` component featuring a collapsible sidebar with inset and secondary navigation. - Created supporting components: `Sidebar`, `SidebarContent`, `SidebarFooter`, `SidebarGroup`, `SidebarGroupContent`, `SidebarGroupLabel`, `SidebarHeader`, `SidebarInset`, `SidebarMenu`, `SidebarMenuItem`, `SidebarMenuButton`, `SidebarMenuSub`, `SidebarMenuSubItem`, `SidebarProvider`, `SidebarRail`, `SidebarSeparator`, and `SidebarTrigger`. - Enhanced sidebar functionality with mobile responsiveness and customizable attributes. - Improved layout and styling using Tailwind CSS for a cohesive design. ### Impact - Provides a comprehensive sidebar navigation solution, improving user experience and accessibility within the ShellUI framework. --- .../Components/AppSidebar.razor | 338 ++++++++++++++++++ .../Components/Sidebar.razor | 140 +++++++- .../Components/SidebarContent.razor | 14 + .../Components/SidebarFooter.razor | 14 + .../Components/SidebarGroup.razor | 14 + .../Components/SidebarGroupContent.razor | 14 + .../Components/SidebarGroupLabel.razor | 17 + .../Components/SidebarHeader.razor | 14 + .../Components/SidebarInset.razor | 16 + .../Components/SidebarMenu.razor | 14 + .../Components/SidebarMenuAction.razor | 23 ++ .../Components/SidebarMenuBadge.razor | 18 + .../Components/SidebarMenuButton.razor | 59 +++ .../Components/SidebarMenuItem.razor | 14 + .../Components/SidebarMenuSub.razor | 17 + .../Components/SidebarMenuSubButton.razor | 43 +++ .../Components/SidebarMenuSubItem.razor | 14 + .../Components/SidebarProvider.razor | 119 ++++++ .../Components/SidebarRail.razor | 29 ++ .../Components/SidebarSeparator.razor | 11 + .../Components/SidebarTrigger.razor | 27 ++ 21 files changed, 964 insertions(+), 5 deletions(-) create mode 100644 src/ShellUI.Components/Components/AppSidebar.razor create mode 100644 src/ShellUI.Components/Components/SidebarContent.razor create mode 100644 src/ShellUI.Components/Components/SidebarFooter.razor create mode 100644 src/ShellUI.Components/Components/SidebarGroup.razor create mode 100644 src/ShellUI.Components/Components/SidebarGroupContent.razor create mode 100644 src/ShellUI.Components/Components/SidebarGroupLabel.razor create mode 100644 src/ShellUI.Components/Components/SidebarHeader.razor create mode 100644 src/ShellUI.Components/Components/SidebarInset.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenu.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenuAction.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenuBadge.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenuButton.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenuItem.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenuSub.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenuSubButton.razor create mode 100644 src/ShellUI.Components/Components/SidebarMenuSubItem.razor create mode 100644 src/ShellUI.Components/Components/SidebarProvider.razor create mode 100644 src/ShellUI.Components/Components/SidebarRail.razor create mode 100644 src/ShellUI.Components/Components/SidebarSeparator.razor create mode 100644 src/ShellUI.Components/Components/SidebarTrigger.razor diff --git a/src/ShellUI.Components/Components/AppSidebar.razor b/src/ShellUI.Components/Components/AppSidebar.razor new file mode 100644 index 0000000..70e279e --- /dev/null +++ b/src/ShellUI.Components/Components/AppSidebar.razor @@ -0,0 +1,338 @@ +@namespace ShellUI.Components + +@* ────────────────────────────────────────────────────────── + AppSidebar — shadcn sidebar-08 (Inset + Secondary Nav) + + Usage: + + + +
+
+ + + ... +
+
+
+ +
+
+
+ ────────────────────────────────────────────────────────── *@ + + + @* ── Header: Logo & Company ── *@ + + + + +
+ +
+
+ Acme Inc + Enterprise +
+
+
+
+
+ + @* ── Content ── *@ + + + @* ─── NavMain: Platform ─── *@ + + Platform + + + @foreach (var item in _navMainItems) + { + + + + @item.Title + + @if (item.SubItems?.Count > 0) + { + + + + @if (item.IsExpanded) + { + + @foreach (var sub in item.SubItems) + { + + + @sub.Title + + + } + + } + } + + } + + + + + @* ─── NavProjects ─── *@ + + Projects + + + @foreach (var project in _projects) + { + + + + @project.Name + + + + More + + + } + + + + More + + + + + + + @* ─── NavSecondary (pushed to bottom) ─── *@ + + + + @foreach (var item in _secondaryItems) + { + + + + @item.Title + + + } + + + + + + + @* ── Footer: User Profile ── *@ + + + +
+ + + @if (_showUserMenu) + { + +
+ + +
+
+ +
+ @_user.Name + @_user.Email +
+
+
+ +
+ + + +
+ +
+ } +
+
+
+
+
+ +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _showUserMenu; + + private void ToggleUserMenu() => _showUserMenu = !_showUserMenu; + private void ToggleExpand(NavItem item) { item.IsExpanded = !item.IsExpanded; } + + // ── Data Models ── + private class NavItem + { + public string Title { get; set; } = ""; + public string Url { get; set; } = "#"; + public string IconClass { get; set; } = ""; + public bool IsActive { get; set; } + public bool IsExpanded { get; set; } + public List? SubItems { get; set; } + } + + private class SubNavItem + { + public string Title { get; set; } = ""; + public string Url { get; set; } = "#"; + public bool IsActive { get; set; } + } + + private class ProjectItem + { + public string Name { get; set; } = ""; + public string Url { get; set; } = "#"; + public string IconClass { get; set; } = ""; + } + + private class UserProfile + { + public string Name { get; set; } = ""; + public string Email { get; set; } = ""; + public string Initials { get; set; } = ""; + } + + // ── User ── + private readonly UserProfile _user = new() + { + Name = "shadcn", + Email = "m@example.com", + Initials = "CN" + }; + + // ── Navigation Data ── + private readonly List _navMainItems = new() + { + new NavItem + { + Title = "Playground", + Url = "#", + IsActive = true, + IsExpanded = true, + IconClass = "fa-solid fa-code", + SubItems = new() + { + new SubNavItem { Title = "History", Url = "#" }, + new SubNavItem { Title = "Starred", Url = "#" }, + new SubNavItem { Title = "Settings", Url = "#" } + } + }, + new NavItem + { + Title = "Models", + Url = "#", + IconClass = "fa-solid fa-microchip", + SubItems = new() + { + new SubNavItem { Title = "Genesis", Url = "#" }, + new SubNavItem { Title = "Explorer", Url = "#" }, + new SubNavItem { Title = "Quantum", Url = "#" } + } + }, + new NavItem + { + Title = "Documentation", + Url = "#", + IconClass = "fa-solid fa-book", + SubItems = new() + { + new SubNavItem { Title = "Introduction", Url = "#" }, + new SubNavItem { Title = "Get Started", Url = "#" }, + new SubNavItem { Title = "Tutorials", Url = "#" }, + new SubNavItem { Title = "Changelog", Url = "#" } + } + }, + new NavItem + { + Title = "Settings", + Url = "#", + IconClass = "fa-solid fa-gear", + SubItems = new() + { + new SubNavItem { Title = "General", Url = "#" }, + new SubNavItem { Title = "Team", Url = "#" }, + new SubNavItem { Title = "Billing", Url = "#" }, + new SubNavItem { Title = "Limits", Url = "#" } + } + } + }; + + private readonly List _projects = new() + { + new ProjectItem + { + Name = "Design Engineering", + Url = "#", + IconClass = "fa-solid fa-pen-ruler" + }, + new ProjectItem + { + Name = "Sales & Marketing", + Url = "#", + IconClass = "fa-solid fa-bullhorn" + }, + new ProjectItem + { + Name = "Travel", + Url = "#", + IconClass = "fa-solid fa-location-dot" + } + }; + + private readonly List _secondaryItems = new() + { + new NavItem + { + Title = "Support", + Url = "#", + IconClass = "fa-solid fa-life-ring" + }, + new NavItem + { + Title = "Feedback", + Url = "#", + IconClass = "fa-solid fa-paper-plane" + } + }; +} diff --git a/src/ShellUI.Components/Components/Sidebar.razor b/src/ShellUI.Components/Components/Sidebar.razor index 2724c3e..e2e3704 100644 --- a/src/ShellUI.Components/Components/Sidebar.razor +++ b/src/ShellUI.Components/Components/Sidebar.razor @@ -1,15 +1,145 @@ @namespace ShellUI.Components - +} +else if (SidebarProvider?.IsMobile == true) +{ + @if (SidebarProvider.MobileOpen) + { + +
+ + +
+
+ @ChildContent +
+
+ } +} +else +{ + + +} @code { - [Parameter] public bool IsOpen { get; set; } = true; + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public SidebarSide Side { get; set; } = SidebarSide.Left; + [Parameter] public SidebarVariant Variant { get; set; } = SidebarVariant.Sidebar; + [Parameter] public SidebarCollapsible Collapsible { get; set; } = SidebarCollapsible.Offcanvas; [Parameter] public RenderFragment? ChildContent { get; set; } - [Parameter] public string ClassName { get; set; } = ""; + [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + + private bool IsFloatingOrInset => Variant == SidebarVariant.Floating || Variant == SidebarVariant.Inset; + + // ── Inline styles for CSS variable widths (avoids Tailwind arbitrary value issues) ── + + private string GapStyle + { + get + { + var state = SidebarProvider?.State ?? "expanded"; + var isCollapsed = state == "collapsed"; + + if (isCollapsed && Collapsible == SidebarCollapsible.Offcanvas) + return "width: 0;"; + if (isCollapsed && Collapsible == SidebarCollapsible.Icon) + return IsFloatingOrInset + ? "width: calc(var(--sidebar-width-icon) + 1rem);" + : "width: var(--sidebar-width-icon);"; + return "width: var(--sidebar-width);"; + } + } + + private string FixedStyle + { + get + { + var state = SidebarProvider?.State ?? "expanded"; + var isCollapsed = state == "collapsed"; + var parts = new List(); + + // Width + if (isCollapsed && Collapsible == SidebarCollapsible.Icon) + parts.Add(IsFloatingOrInset + ? "width: calc(var(--sidebar-width-icon) + 1rem + 2px)" + : "width: var(--sidebar-width-icon)"); + else + parts.Add("width: var(--sidebar-width)"); + + // Position + if (Side == SidebarSide.Left) + { + parts.Add(isCollapsed && Collapsible == SidebarCollapsible.Offcanvas + ? "left: calc(var(--sidebar-width) * -1)" + : "left: 0"); + } + else + { + parts.Add(isCollapsed && Collapsible == SidebarCollapsible.Offcanvas + ? "right: calc(var(--sidebar-width) * -1)" + : "right: 0"); + } + + return string.Join("; ", parts) + ";"; + } + } + + // ── Tailwind classes (no CSS variable arbitrary values) ── + + private string GapClass => Shell.Cn( + "relative h-svh bg-transparent transition-[width] ease-linear duration-200", + Side == SidebarSide.Right ? "rotate-180" : null + ); + + private string FixedClass => Shell.Cn( + "fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] ease-linear duration-200 md:flex", + IsFloatingOrInset ? "p-2" : null, + !IsFloatingOrInset && Side == SidebarSide.Left ? "border-r" : null, + !IsFloatingOrInset && Side == SidebarSide.Right ? "border-l" : null, + Class + ); } diff --git a/src/ShellUI.Components/Components/SidebarContent.razor b/src/ShellUI.Components/Components/SidebarContent.razor new file mode 100644 index 0000000..b084f5b --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarContent.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarFooter.razor b/src/ShellUI.Components/Components/SidebarFooter.razor new file mode 100644 index 0000000..e807ab3 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarFooter.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarGroup.razor b/src/ShellUI.Components/Components/SidebarGroup.razor new file mode 100644 index 0000000..e9f4c3d --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarGroup.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarGroupContent.razor b/src/ShellUI.Components/Components/SidebarGroupContent.razor new file mode 100644 index 0000000..e8cfd87 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarGroupContent.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarGroupLabel.razor b/src/ShellUI.Components/Components/SidebarGroupLabel.razor new file mode 100644 index 0000000..ddfbadb --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarGroupLabel.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + +
svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + Class)" + @attributes="AdditionalAttributes"> + @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarHeader.razor b/src/ShellUI.Components/Components/SidebarHeader.razor new file mode 100644 index 0000000..8bb250a --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarHeader.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarInset.razor b/src/ShellUI.Components/Components/SidebarInset.razor new file mode 100644 index 0000000..6a0f863 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarInset.razor @@ -0,0 +1,16 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenu.razor b/src/ShellUI.Components/Components/SidebarMenu.razor new file mode 100644 index 0000000..f0881f1 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenu.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenuAction.razor b/src/ShellUI.Components/Components/SidebarMenuAction.razor new file mode 100644 index 0000000..031d791 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenuAction.razor @@ -0,0 +1,23 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public bool ShowOnHover { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenuBadge.razor b/src/ShellUI.Components/Components/SidebarMenuBadge.razor new file mode 100644 index 0000000..d896354 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenuBadge.razor @@ -0,0 +1,18 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenuButton.razor b/src/ShellUI.Components/Components/SidebarMenuButton.razor new file mode 100644 index 0000000..b271890 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenuButton.razor @@ -0,0 +1,59 @@ +@namespace ShellUI.Components + +@{ + var sizeClass = Size switch + { + "sm" => "text-xs", + "lg" => "text-sm group-data-[collapsible=icon]:!p-0", + _ => "" + }; + + var buttonClass = Shell.Cn( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center", + sizeClass, + Class + ); + + var isIconMode = SidebarProvider != null && !SidebarProvider.IsMobile && SidebarProvider.State == "collapsed"; + var tooltipTitle = isIconMode && !string.IsNullOrEmpty(Tooltip) ? Tooltip : null; +} + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public string? Href { get; set; } + [Parameter] public string Size { get; set; } = "default"; + [Parameter] public bool IsActive { get; set; } + [Parameter] public string? Tooltip { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenuItem.razor b/src/ShellUI.Components/Components/SidebarMenuItem.razor new file mode 100644 index 0000000..79ce063 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenuItem.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
  • + @ChildContent +
  • + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenuSub.razor b/src/ShellUI.Components/Components/SidebarMenuSub.razor new file mode 100644 index 0000000..e220376 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenuSub.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + +
      + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenuSubButton.razor b/src/ShellUI.Components/Components/SidebarMenuSubButton.razor new file mode 100644 index 0000000..695d252 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenuSubButton.razor @@ -0,0 +1,43 @@ +@namespace ShellUI.Components + +@{ + var buttonClass = Shell.Cn( + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + Size == "sm" ? "text-xs" : "text-sm", + IsActive ? "bg-sidebar-accent text-sidebar-accent-foreground" : "", + Class + ); +} + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public string Size { get; set; } = "md"; + [Parameter] public bool IsActive { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarMenuSubItem.razor b/src/ShellUI.Components/Components/SidebarMenuSubItem.razor new file mode 100644 index 0000000..55a7fd3 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarMenuSubItem.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + +
  • + @ChildContent +
  • + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarProvider.razor b/src/ShellUI.Components/Components/SidebarProvider.razor new file mode 100644 index 0000000..273cd28 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarProvider.razor @@ -0,0 +1,119 @@ +@namespace ShellUI.Components +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime +@implements IAsyncDisposable + +
    + + @ChildContent + +
    + +@code { + private IJSObjectReference? _module; + private IJSObjectReference? _handlers; + private DotNetObjectReference? _dotnetRef; + + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public bool DefaultOpen { get; set; } = true; + [Parameter] public string Width { get; set; } = "16rem"; + [Parameter] public string WidthIcon { get; set; } = "3rem"; + [Parameter] public string? Class { get; set; } + [Parameter] public string? Style { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + // Public state + public bool IsMobile { get; private set; } + public bool IsOpen { get; private set; } = true; + public bool MobileOpen { get; private set; } + public string State => (IsMobile ? MobileOpen : IsOpen) ? "expanded" : "collapsed"; + + private string StyleString + { + get + { + var s = $"--sidebar-width: {Width}; --sidebar-width-icon: {WidthIcon};"; + if (!string.IsNullOrEmpty(Style)) s += " " + Style; + return s; + } + } + + private string WrapperClass => Shell.Cn( + "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", + Class + ); + + protected override void OnInitialized() + { + IsOpen = DefaultOpen; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _dotnetRef = DotNetObjectReference.Create(this); + try + { + _module = await JSRuntime.InvokeAsync( + "import", "./_content/ShellUI.Components/shellui-sidebar.js"); + _handlers = await _module.InvokeAsync( + "initSidebar", _dotnetRef); + } + catch { /* Pre-render or JS not available */ } + } + } + + [JSInvokable] + public void OnMobileChanged(bool isMobile) + { + if (IsMobile != isMobile) + { + IsMobile = isMobile; + if (isMobile) MobileOpen = false; + StateHasChanged(); + } + } + + [JSInvokable] + public Task OnToggle() + { + Toggle(); + return Task.CompletedTask; + } + + public void Toggle() + { + if (IsMobile) MobileOpen = !MobileOpen; + else IsOpen = !IsOpen; + StateHasChanged(); + } + + public void SetOpen(bool open) + { + if (IsMobile) MobileOpen = open; + else IsOpen = open; + StateHasChanged(); + } + + public void OpenMobileSidebar() { MobileOpen = true; StateHasChanged(); } + public void CloseMobileSidebar() { MobileOpen = false; StateHasChanged(); } + + public async ValueTask DisposeAsync() + { + try + { + if (_handlers != null) + { + await _handlers.InvokeVoidAsync("dispose"); + await _handlers.DisposeAsync(); + } + if (_module != null) await _module.DisposeAsync(); + } + catch { } + _dotnetRef?.Dispose(); + } +} diff --git a/src/ShellUI.Components/Components/SidebarRail.razor b/src/ShellUI.Components/Components/SidebarRail.razor new file mode 100644 index 0000000..43e109c --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarRail.razor @@ -0,0 +1,29 @@ +@namespace ShellUI.Components + + + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private void HandleClick() + { + SidebarProvider?.Toggle(); + } +} diff --git a/src/ShellUI.Components/Components/SidebarSeparator.razor b/src/ShellUI.Components/Components/SidebarSeparator.razor new file mode 100644 index 0000000..b9ae802 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarSeparator.razor @@ -0,0 +1,11 @@ +@namespace ShellUI.Components + +
    + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SidebarTrigger.razor b/src/ShellUI.Components/Components/SidebarTrigger.razor new file mode 100644 index 0000000..e6606b9 --- /dev/null +++ b/src/ShellUI.Components/Components/SidebarTrigger.razor @@ -0,0 +1,27 @@ +@namespace ShellUI.Components + + + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick(MouseEventArgs args) + { + await OnClick.InvokeAsync(args); + SidebarProvider?.Toggle(); + } +} From 488ea40a7c768e66a89d9cb7faf71e564186adc1 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 11 Feb 2026 01:17:30 +0200 Subject: [PATCH 02/49] feat: implement AppSidebar and supporting components for enhanced navigation ### Summary - Introduced `AppSidebar` component with collapsible functionality and secondary navigation. - Created supporting components: `Sidebar`, `SidebarContent`, `SidebarFooter`, `SidebarGroup`, `SidebarGroupContent`, `SidebarGroupLabel`, `SidebarHeader`, `SidebarInset`, `SidebarMenu`, `SidebarMenuItem`, `SidebarMenuButton`, `SidebarMenuSub`, `SidebarMenuSubItem`, `SidebarProvider`, `SidebarRail`, `SidebarSeparator`, and `SidebarTrigger`. - Enhanced mobile responsiveness and customizable attributes for improved user experience. - Utilized Tailwind CSS for cohesive styling and layout. ### Impact - Provides a comprehensive sidebar navigation solution, improving accessibility and usability within the ShellUI framework. --- .../Components/UI/AppSidebar.razor | 338 ++++++++++++++++++ .../Components/UI/Sidebar.razor | 142 +++++++- .../Components/UI/SidebarContent.razor | 14 + .../Components/UI/SidebarFooter.razor | 14 + .../Components/UI/SidebarGroup.razor | 14 + .../Components/UI/SidebarGroupContent.razor | 14 + .../Components/UI/SidebarGroupLabel.razor | 17 + .../Components/UI/SidebarHeader.razor | 14 + .../Components/UI/SidebarInset.razor | 16 + .../Components/UI/SidebarMenu.razor | 14 + .../Components/UI/SidebarMenuAction.razor | 23 ++ .../Components/UI/SidebarMenuBadge.razor | 18 + .../Components/UI/SidebarMenuButton.razor | 59 +++ .../Components/UI/SidebarMenuItem.razor | 14 + .../Components/UI/SidebarMenuSub.razor | 17 + .../Components/UI/SidebarMenuSubButton.razor | 43 +++ .../Components/UI/SidebarMenuSubItem.razor | 14 + .../Components/UI/SidebarModels.cs | 21 ++ .../Components/UI/SidebarProvider.razor | 119 ++++++ .../Components/UI/SidebarRail.razor | 29 ++ .../Components/UI/SidebarSeparator.razor | 11 + .../Components/UI/SidebarTrigger.razor | 27 ++ .../Templates/SidebarContentTemplate.cs | 32 ++ .../Templates/SidebarFooterTemplate.cs | 32 ++ .../Templates/SidebarGroupContentTemplate.cs | 32 ++ .../Templates/SidebarGroupLabelTemplate.cs | 35 ++ .../Templates/SidebarGroupTemplate.cs | 32 ++ .../Templates/SidebarHeaderTemplate.cs | 32 ++ .../Templates/SidebarInsetTemplate.cs | 34 ++ .../Templates/SidebarJsTemplate.cs | 56 +++ .../Templates/SidebarMenuActionTemplate.cs | 41 +++ .../Templates/SidebarMenuBadgeTemplate.cs | 36 ++ .../Templates/SidebarMenuButtonTemplate.cs | 77 ++++ .../Templates/SidebarMenuItemTemplate.cs | 32 ++ .../Templates/SidebarMenuSubButtonTemplate.cs | 61 ++++ .../Templates/SidebarMenuSubItemTemplate.cs | 32 ++ .../Templates/SidebarMenuSubTemplate.cs | 35 ++ .../Templates/SidebarMenuTemplate.cs | 32 ++ .../Templates/SidebarModelsTemplate.cs | 39 ++ .../Templates/SidebarProviderTemplate.cs | 138 +++++++ .../Templates/SidebarRailTemplate.cs | 47 +++ .../Templates/SidebarSeparatorTemplate.cs | 29 ++ .../Templates/SidebarTemplate.cs | 153 +++++++- .../Templates/SidebarTriggerTemplate.cs | 45 +++ 44 files changed, 2055 insertions(+), 19 deletions(-) create mode 100644 NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarContent.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarFooter.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarGroup.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarGroupContent.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarGroupLabel.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarHeader.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarInset.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenu.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenuAction.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenuBadge.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenuItem.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSub.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubButton.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubItem.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarModels.cs create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarProvider.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarRail.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarSeparator.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/SidebarTrigger.razor create mode 100644 src/ShellUI.Templates/Templates/SidebarContentTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarFooterTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarGroupContentTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarGroupLabelTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarGroupTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarHeaderTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarJsTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuActionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuBadgeTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuButtonTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuSubButtonTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuSubItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuSubTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarMenuTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarModelsTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarProviderTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarRailTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarSeparatorTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarTriggerTemplate.cs diff --git a/NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor b/NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor new file mode 100644 index 0000000..5b112c8 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor @@ -0,0 +1,338 @@ +@namespace BlazorInteractiveServer.Components.UI + +@* ────────────────────────────────────────────────────────── + AppSidebar — shadcn sidebar-08 (Inset + Secondary Nav) + + Usage: + + + +
    +
    + + + ... +
    +
    +
    + +
    +
    +
    + ────────────────────────────────────────────────────────── *@ + + + @* ── Header: Logo & Company ── *@ + + + + +
    + +
    +
    + Acme Inc + Enterprise +
    +
    +
    +
    +
    + + @* ── Content ── *@ + + + @* ─── NavMain: Platform ─── *@ + + Platform + + + @foreach (var item in _navMainItems) + { + + + + @item.Title + + @if (item.SubItems?.Count > 0) + { + + + + @if (item.IsExpanded) + { + + @foreach (var sub in item.SubItems) + { + + + @sub.Title + + + } + + } + } + + } + + + + + @* ─── NavProjects ─── *@ + + Projects + + + @foreach (var project in _projects) + { + + + + @project.Name + + + + More + + + } + + + + More + + + + + + + @* ─── NavSecondary (pushed to bottom) ─── *@ + + + + @foreach (var item in _secondaryItems) + { + + + + @item.Title + + + } + + + + + + + @* ── Footer: User Profile ── *@ + + + +
    + + + @if (_showUserMenu) + { + +
    + + +
    +
    + +
    + @_user.Name + @_user.Email +
    +
    +
    + +
    + + + +
    + +
    + } +
    +
    +
    +
    +
    + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _showUserMenu; + + private void ToggleUserMenu() => _showUserMenu = !_showUserMenu; + private void ToggleExpand(NavItem item) { item.IsExpanded = !item.IsExpanded; } + + // ── Data Models ── + private class NavItem + { + public string Title { get; set; } = ""; + public string Url { get; set; } = "#"; + public string IconClass { get; set; } = ""; + public bool IsActive { get; set; } + public bool IsExpanded { get; set; } + public List? SubItems { get; set; } + } + + private class SubNavItem + { + public string Title { get; set; } = ""; + public string Url { get; set; } = "#"; + public bool IsActive { get; set; } + } + + private class ProjectItem + { + public string Name { get; set; } = ""; + public string Url { get; set; } = "#"; + public string IconClass { get; set; } = ""; + } + + private class UserProfile + { + public string Name { get; set; } = ""; + public string Email { get; set; } = ""; + public string Initials { get; set; } = ""; + } + + // ── User ── + private readonly UserProfile _user = new() + { + Name = "shadcn", + Email = "m@example.com", + Initials = "CN" + }; + + // ── Navigation Data ── + private readonly List _navMainItems = new() + { + new NavItem + { + Title = "Playground", + Url = "#", + IsActive = true, + IsExpanded = true, + IconClass = "fa-solid fa-code", + SubItems = new() + { + new SubNavItem { Title = "History", Url = "#" }, + new SubNavItem { Title = "Starred", Url = "#" }, + new SubNavItem { Title = "Settings", Url = "#" } + } + }, + new NavItem + { + Title = "Models", + Url = "#", + IconClass = "fa-solid fa-microchip", + SubItems = new() + { + new SubNavItem { Title = "Genesis", Url = "#" }, + new SubNavItem { Title = "Explorer", Url = "#" }, + new SubNavItem { Title = "Quantum", Url = "#" } + } + }, + new NavItem + { + Title = "Documentation", + Url = "#", + IconClass = "fa-solid fa-book", + SubItems = new() + { + new SubNavItem { Title = "Introduction", Url = "#" }, + new SubNavItem { Title = "Get Started", Url = "#" }, + new SubNavItem { Title = "Tutorials", Url = "#" }, + new SubNavItem { Title = "Changelog", Url = "#" } + } + }, + new NavItem + { + Title = "Settings", + Url = "#", + IconClass = "fa-solid fa-gear", + SubItems = new() + { + new SubNavItem { Title = "General", Url = "#" }, + new SubNavItem { Title = "Team", Url = "#" }, + new SubNavItem { Title = "Billing", Url = "#" }, + new SubNavItem { Title = "Limits", Url = "#" } + } + } + }; + + private readonly List _projects = new() + { + new ProjectItem + { + Name = "Design Engineering", + Url = "#", + IconClass = "fa-solid fa-pen-ruler" + }, + new ProjectItem + { + Name = "Sales & Marketing", + Url = "#", + IconClass = "fa-solid fa-bullhorn" + }, + new ProjectItem + { + Name = "Travel", + Url = "#", + IconClass = "fa-solid fa-location-dot" + } + }; + + private readonly List _secondaryItems = new() + { + new NavItem + { + Title = "Support", + Url = "#", + IconClass = "fa-solid fa-life-ring" + }, + new NavItem + { + Title = "Feedback", + Url = "#", + IconClass = "fa-solid fa-paper-plane" + } + }; +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor b/NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor index b43bf82..936b85f 100644 --- a/NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor +++ b/NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor @@ -1,17 +1,145 @@ @namespace BlazorInteractiveServer.Components.UI -@using BlazorInteractiveServer.Components - +} +else if (SidebarProvider?.IsMobile == true) +{ + @if (SidebarProvider.MobileOpen) + { + +
    + + +
    +
    + @ChildContent +
    +
    + } +} +else +{ + + +} @code { - [Parameter] public bool IsOpen { get; set; } = true; + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public SidebarSide Side { get; set; } = SidebarSide.Left; + [Parameter] public SidebarVariant Variant { get; set; } = SidebarVariant.Sidebar; + [Parameter] public SidebarCollapsible Collapsible { get; set; } = SidebarCollapsible.Offcanvas; [Parameter] public RenderFragment? ChildContent { get; set; } - [Parameter] public string ClassName { get; set; } = ""; + [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } -} + private bool IsFloatingOrInset => Variant == SidebarVariant.Floating || Variant == SidebarVariant.Inset; + + // ── Inline styles for CSS variable widths (avoids Tailwind arbitrary value issues) ── + + private string GapStyle + { + get + { + var state = SidebarProvider?.State ?? "expanded"; + var isCollapsed = state == "collapsed"; + + if (isCollapsed && Collapsible == SidebarCollapsible.Offcanvas) + return "width: 0;"; + if (isCollapsed && Collapsible == SidebarCollapsible.Icon) + return IsFloatingOrInset + ? "width: calc(var(--sidebar-width-icon) + 1rem);" + : "width: var(--sidebar-width-icon);"; + return "width: var(--sidebar-width);"; + } + } + + private string FixedStyle + { + get + { + var state = SidebarProvider?.State ?? "expanded"; + var isCollapsed = state == "collapsed"; + var parts = new List(); + + // Width + if (isCollapsed && Collapsible == SidebarCollapsible.Icon) + parts.Add(IsFloatingOrInset + ? "width: calc(var(--sidebar-width-icon) + 1rem + 2px)" + : "width: var(--sidebar-width-icon)"); + else + parts.Add("width: var(--sidebar-width)"); + + // Position + if (Side == SidebarSide.Left) + { + parts.Add(isCollapsed && Collapsible == SidebarCollapsible.Offcanvas + ? "left: calc(var(--sidebar-width) * -1)" + : "left: 0"); + } + else + { + parts.Add(isCollapsed && Collapsible == SidebarCollapsible.Offcanvas + ? "right: calc(var(--sidebar-width) * -1)" + : "right: 0"); + } + + return string.Join("; ", parts) + ";"; + } + } + + // ── Tailwind classes (no CSS variable arbitrary values) ── + + private string GapClass => Shell.Cn( + "relative h-svh bg-transparent transition-[width] ease-linear duration-200", + Side == SidebarSide.Right ? "rotate-180" : null + ); + + private string FixedClass => Shell.Cn( + "fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] ease-linear duration-200 md:flex", + IsFloatingOrInset ? "p-2" : null, + !IsFloatingOrInset && Side == SidebarSide.Left ? "border-r" : null, + !IsFloatingOrInset && Side == SidebarSide.Right ? "border-l" : null, + Class + ); +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarContent.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarContent.razor new file mode 100644 index 0000000..21b0914 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarContent.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarFooter.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarFooter.razor new file mode 100644 index 0000000..ccbd2f5 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarFooter.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarGroup.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarGroup.razor new file mode 100644 index 0000000..2c7f571 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarGroup.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarGroupContent.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarGroupContent.razor new file mode 100644 index 0000000..d261733 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarGroupContent.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarGroupLabel.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarGroupLabel.razor new file mode 100644 index 0000000..7202371 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarGroupLabel.razor @@ -0,0 +1,17 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + Class)" + @attributes="AdditionalAttributes"> + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarHeader.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarHeader.razor new file mode 100644 index 0000000..36d9ecb --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarHeader.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarInset.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarInset.razor new file mode 100644 index 0000000..f0ad979 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarInset.razor @@ -0,0 +1,16 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenu.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenu.razor new file mode 100644 index 0000000..dda358f --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenu.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
      + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuAction.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuAction.razor new file mode 100644 index 0000000..986fd86 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuAction.razor @@ -0,0 +1,23 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] public bool ShowOnHover { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuBadge.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuBadge.razor new file mode 100644 index 0000000..7e22209 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuBadge.razor @@ -0,0 +1,18 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor new file mode 100644 index 0000000..6e429e3 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor @@ -0,0 +1,59 @@ +@namespace BlazorInteractiveServer.Components.UI + +@{ + var sizeClass = Size switch + { + "sm" => "text-xs", + "lg" => "text-sm group-data-[collapsible=icon]:!p-0", + _ => "" + }; + + var buttonClass = Shell.Cn( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center", + sizeClass, + Class + ); + + var isIconMode = SidebarProvider != null && !SidebarProvider.IsMobile && SidebarProvider.State == "collapsed"; + var tooltipTitle = isIconMode && !string.IsNullOrEmpty(Tooltip) ? Tooltip : null; +} + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public string? Href { get; set; } + [Parameter] public string Size { get; set; } = "default"; + [Parameter] public bool IsActive { get; set; } + [Parameter] public string? Tooltip { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuItem.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuItem.razor new file mode 100644 index 0000000..c3a07d7 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuItem.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
  • + @ChildContent +
  • + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSub.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSub.razor new file mode 100644 index 0000000..f547db4 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSub.razor @@ -0,0 +1,17 @@ +@namespace BlazorInteractiveServer.Components.UI + +
      + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubButton.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubButton.razor new file mode 100644 index 0000000..13b14f1 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubButton.razor @@ -0,0 +1,43 @@ +@namespace BlazorInteractiveServer.Components.UI + +@{ + var buttonClass = Shell.Cn( + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + Size == "sm" ? "text-xs" : "text-sm", + IsActive ? "bg-sidebar-accent text-sidebar-accent-foreground" : "", + Class + ); +} + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public string Size { get; set; } = "md"; + [Parameter] public bool IsActive { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubItem.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubItem.razor new file mode 100644 index 0000000..93952f8 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuSubItem.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
  • + @ChildContent +
  • + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarModels.cs b/NET9/BlazorInteractiveServer/Components/UI/SidebarModels.cs new file mode 100644 index 0000000..671ad91 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarModels.cs @@ -0,0 +1,21 @@ +namespace BlazorInteractiveServer.Components.UI; + +public enum SidebarVariant +{ + Sidebar, + Floating, + Inset +} + +public enum SidebarCollapsible +{ + Offcanvas, + Icon, + None +} + +public enum SidebarSide +{ + Left, + Right +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarProvider.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarProvider.razor new file mode 100644 index 0000000..dc8cf71 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarProvider.razor @@ -0,0 +1,119 @@ +@namespace BlazorInteractiveServer.Components.UI +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime +@implements IAsyncDisposable + +
    + + @ChildContent + +
    + +@code { + private IJSObjectReference? _module; + private IJSObjectReference? _handlers; + private DotNetObjectReference? _dotnetRef; + + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public bool DefaultOpen { get; set; } = true; + [Parameter] public string Width { get; set; } = "16rem"; + [Parameter] public string WidthIcon { get; set; } = "3rem"; + [Parameter] public string? Class { get; set; } + [Parameter] public string? Style { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + // Public state + public bool IsMobile { get; private set; } + public bool IsOpen { get; private set; } = true; + public bool MobileOpen { get; private set; } + public string State => (IsMobile ? MobileOpen : IsOpen) ? "expanded" : "collapsed"; + + private string StyleString + { + get + { + var s = $"--sidebar-width: {Width}; --sidebar-width-icon: {WidthIcon};"; + if (!string.IsNullOrEmpty(Style)) s += " " + Style; + return s; + } + } + + private string WrapperClass => Shell.Cn( + "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", + Class + ); + + protected override void OnInitialized() + { + IsOpen = DefaultOpen; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _dotnetRef = DotNetObjectReference.Create(this); + try + { + _module = await JSRuntime.InvokeAsync( + "import", "./shellui-sidebar.js"); + _handlers = await _module.InvokeAsync( + "initSidebar", _dotnetRef); + } + catch { /* Pre-render or JS not available */ } + } + } + + [JSInvokable] + public void OnMobileChanged(bool isMobile) + { + if (IsMobile != isMobile) + { + IsMobile = isMobile; + if (isMobile) MobileOpen = false; + StateHasChanged(); + } + } + + [JSInvokable] + public Task OnToggle() + { + Toggle(); + return Task.CompletedTask; + } + + public void Toggle() + { + if (IsMobile) MobileOpen = !MobileOpen; + else IsOpen = !IsOpen; + StateHasChanged(); + } + + public void SetOpen(bool open) + { + if (IsMobile) MobileOpen = open; + else IsOpen = open; + StateHasChanged(); + } + + public void OpenMobileSidebar() { MobileOpen = true; StateHasChanged(); } + public void CloseMobileSidebar() { MobileOpen = false; StateHasChanged(); } + + public async ValueTask DisposeAsync() + { + try + { + if (_handlers != null) + { + await _handlers.InvokeVoidAsync("dispose"); + await _handlers.DisposeAsync(); + } + if (_module != null) await _module.DisposeAsync(); + } + catch { } + _dotnetRef?.Dispose(); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarRail.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarRail.razor new file mode 100644 index 0000000..639dcff --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarRail.razor @@ -0,0 +1,29 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private void HandleClick() + { + SidebarProvider?.Toggle(); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarSeparator.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarSeparator.razor new file mode 100644 index 0000000..9446161 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarSeparator.razor @@ -0,0 +1,11 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarTrigger.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarTrigger.razor new file mode 100644 index 0000000..bfadb4e --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarTrigger.razor @@ -0,0 +1,27 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick(MouseEventArgs args) + { + await OnClick.InvokeAsync(args); + SidebarProvider?.Toggle(); + } +} diff --git a/src/ShellUI.Templates/Templates/SidebarContentTemplate.cs b/src/ShellUI.Templates/Templates/SidebarContentTemplate.cs new file mode 100644 index 0000000..0acd85f --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarContentTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarContentTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-content", + DisplayName = "Sidebar Content", + Description = "Scrollable content area of the sidebar", + Category = ComponentCategory.Layout, + FilePath = "SidebarContent.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarFooterTemplate.cs b/src/ShellUI.Templates/Templates/SidebarFooterTemplate.cs new file mode 100644 index 0000000..1615967 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarFooterTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarFooterTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-footer", + DisplayName = "Sidebar Footer", + Description = "Footer section of the sidebar", + Category = ComponentCategory.Layout, + FilePath = "SidebarFooter.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarGroupContentTemplate.cs b/src/ShellUI.Templates/Templates/SidebarGroupContentTemplate.cs new file mode 100644 index 0000000..ac57b76 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarGroupContentTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarGroupContentTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-group-content", + DisplayName = "Sidebar Group Content", + Description = "Content wrapper for a sidebar navigation group", + Category = ComponentCategory.Layout, + FilePath = "SidebarGroupContent.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarGroupLabelTemplate.cs b/src/ShellUI.Templates/Templates/SidebarGroupLabelTemplate.cs new file mode 100644 index 0000000..04bab39 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarGroupLabelTemplate.cs @@ -0,0 +1,35 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarGroupLabelTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-group-label", + DisplayName = "Sidebar Group Label", + Description = "Label for a sidebar navigation group", + Category = ComponentCategory.Layout, + FilePath = "SidebarGroupLabel.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    svg]:size-4 [&>svg]:shrink-0"", + ""group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0"", + Class)"" + @attributes=""AdditionalAttributes""> + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarGroupTemplate.cs b/src/ShellUI.Templates/Templates/SidebarGroupTemplate.cs new file mode 100644 index 0000000..32ae546 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarGroupTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarGroupTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-group", + DisplayName = "Sidebar Group", + Description = "Group container for sidebar navigation sections", + Category = ComponentCategory.Layout, + FilePath = "SidebarGroup.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarHeaderTemplate.cs b/src/ShellUI.Templates/Templates/SidebarHeaderTemplate.cs new file mode 100644 index 0000000..bd71a8e --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarHeaderTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarHeaderTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-header", + DisplayName = "Sidebar Header", + Description = "Header section of the sidebar", + Category = ComponentCategory.Layout, + FilePath = "SidebarHeader.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs b/src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs new file mode 100644 index 0000000..95835e0 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs @@ -0,0 +1,34 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarInsetTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-inset", + DisplayName = "Sidebar Inset", + Description = "Main content wrapper that adjusts layout based on sidebar variant", + Category = ComponentCategory.Layout, + FilePath = "SidebarInset.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarJsTemplate.cs b/src/ShellUI.Templates/Templates/SidebarJsTemplate.cs new file mode 100644 index 0000000..f77be85 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarJsTemplate.cs @@ -0,0 +1,56 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarJsTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-js", + DisplayName = "Sidebar JS", + Description = "JavaScript interop module for Sidebar (mobile detection, keyboard shortcuts)", + Category = ComponentCategory.Layout, + FilePath = "../../wwwroot/shellui-sidebar.js", + IsAvailable = false + }; + + public static string Content => @"// ShellUI Sidebar JS Interop Module +// Handles mobile detection, resize events, keyboard shortcuts + +export function initSidebar(dotnetRef) { + const MOBILE_BREAKPOINT = 768; + const checkMobile = () => window.innerWidth < MOBILE_BREAKPOINT; + + // Initial mobile check + dotnetRef.invokeMethodAsync('OnMobileChanged', checkMobile()); + + // Debounced resize handler + let resizeTimer; + const handleResize = () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + dotnetRef.invokeMethodAsync('OnMobileChanged', checkMobile()); + }, 50); + }; + + // Keyboard shortcut: Ctrl/Cmd + B to toggle sidebar + const handleKeydown = (e) => { + if (e.key === 'b' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + dotnetRef.invokeMethodAsync('OnToggle'); + } + }; + + window.addEventListener('resize', handleResize); + document.addEventListener('keydown', handleKeydown); + + return { + dispose: () => { + clearTimeout(resizeTimer); + window.removeEventListener('resize', handleResize); + document.removeEventListener('keydown', handleKeydown); + } + }; +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuActionTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuActionTemplate.cs new file mode 100644 index 0000000..710da8f --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuActionTemplate.cs @@ -0,0 +1,41 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuActionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu-action", + DisplayName = "Sidebar Menu Action", + Description = "Action button overlay for sidebar menu items", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenuAction.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] public bool ShowOnHover { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuBadgeTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuBadgeTemplate.cs new file mode 100644 index 0000000..48d0a03 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuBadgeTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuBadgeTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu-badge", + DisplayName = "Sidebar Menu Badge", + Description = "Notification badge for sidebar menu items", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenuBadge.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuButtonTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuButtonTemplate.cs new file mode 100644 index 0000000..d3a89d6 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuButtonTemplate.cs @@ -0,0 +1,77 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuButtonTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu-button", + DisplayName = "Sidebar Menu Button", + Description = "Interactive button/link for sidebar menu items with tooltip and icon mode support", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenuButton.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@{ + var sizeClass = Size switch + { + ""sm"" => ""text-xs"", + ""lg"" => ""text-sm group-data-[collapsible=icon]:!p-0"", + _ => """" + }; + + var buttonClass = Shell.Cn( + ""peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center"", + sizeClass, + Class + ); + + var isIconMode = SidebarProvider != null && !SidebarProvider.IsMobile && SidebarProvider.State == ""collapsed""; + var tooltipTitle = isIconMode && !string.IsNullOrEmpty(Tooltip) ? Tooltip : null; +} + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public string? Href { get; set; } + [Parameter] public string Size { get; set; } = ""default""; + [Parameter] public bool IsActive { get; set; } + [Parameter] public string? Tooltip { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuItemTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuItemTemplate.cs new file mode 100644 index 0000000..8d66753 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuItemTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu-item", + DisplayName = "Sidebar Menu Item", + Description = "Individual menu item in the sidebar", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenuItem.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
  • + @ChildContent +
  • + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuSubButtonTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuSubButtonTemplate.cs new file mode 100644 index 0000000..507f927 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuSubButtonTemplate.cs @@ -0,0 +1,61 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuSubButtonTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu-sub-button", + DisplayName = "Sidebar Menu Sub Button", + Description = "Interactive button/link for sidebar sub-menu items", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenuSubButton.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@{ + var buttonClass = Shell.Cn( + ""flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground"", + Size == ""sm"" ? ""text-xs"" : ""text-sm"", + IsActive ? ""bg-sidebar-accent text-sidebar-accent-foreground"" : """", + Class + ); +} + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public string Size { get; set; } = ""md""; + [Parameter] public bool IsActive { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuSubItemTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuSubItemTemplate.cs new file mode 100644 index 0000000..e363ef8 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuSubItemTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuSubItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu-sub-item", + DisplayName = "Sidebar Menu Sub Item", + Description = "Individual item in a sidebar sub-menu", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenuSubItem.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
  • + @ChildContent +
  • + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuSubTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuSubTemplate.cs new file mode 100644 index 0000000..8648c68 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuSubTemplate.cs @@ -0,0 +1,35 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuSubTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu-sub", + DisplayName = "Sidebar Menu Sub", + Description = "Nested sub-menu container for sidebar items", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenuSub.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarMenuTemplate.cs b/src/ShellUI.Templates/Templates/SidebarMenuTemplate.cs new file mode 100644 index 0000000..7e8a9d5 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarMenuTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarMenuTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-menu", + DisplayName = "Sidebar Menu", + Description = "Menu list container for sidebar navigation items", + Category = ComponentCategory.Layout, + FilePath = "SidebarMenu.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarModelsTemplate.cs b/src/ShellUI.Templates/Templates/SidebarModelsTemplate.cs new file mode 100644 index 0000000..8b13692 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarModelsTemplate.cs @@ -0,0 +1,39 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarModelsTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-models", + DisplayName = "Sidebar Models", + Description = "Enum models for Sidebar component (variant, collapsible, side)", + Category = ComponentCategory.Layout, + FilePath = "SidebarModels.cs", + IsAvailable = false + }; + + public static string Content => @"namespace YourProjectNamespace.Components.UI; + +public enum SidebarVariant +{ + Sidebar, + Floating, + Inset +} + +public enum SidebarCollapsible +{ + Offcanvas, + Icon, + None +} + +public enum SidebarSide +{ + Left, + Right +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarProviderTemplate.cs b/src/ShellUI.Templates/Templates/SidebarProviderTemplate.cs new file mode 100644 index 0000000..7d58abd --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarProviderTemplate.cs @@ -0,0 +1,138 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarProviderTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-provider", + DisplayName = "Sidebar Provider", + Description = "State provider for Sidebar (manages open/collapsed/mobile state)", + Category = ComponentCategory.Layout, + FilePath = "SidebarProvider.razor", + IsAvailable = false, + Dependencies = new List { "sidebar-models", "sidebar-js" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime +@implements IAsyncDisposable + +
    + + @ChildContent + +
    + +@code { + private IJSObjectReference? _module; + private IJSObjectReference? _handlers; + private DotNetObjectReference? _dotnetRef; + + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public bool DefaultOpen { get; set; } = true; + [Parameter] public string Width { get; set; } = ""16rem""; + [Parameter] public string WidthIcon { get; set; } = ""3rem""; + [Parameter] public string? Class { get; set; } + [Parameter] public string? Style { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + // Public state + public bool IsMobile { get; private set; } + public bool IsOpen { get; private set; } = true; + public bool MobileOpen { get; private set; } + public string State => (IsMobile ? MobileOpen : IsOpen) ? ""expanded"" : ""collapsed""; + + private string StyleString + { + get + { + var s = $""--sidebar-width: {Width}; --sidebar-width-icon: {WidthIcon};""; + if (!string.IsNullOrEmpty(Style)) s += "" "" + Style; + return s; + } + } + + private string WrapperClass => Shell.Cn( + ""group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar"", + Class + ); + + protected override void OnInitialized() + { + IsOpen = DefaultOpen; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _dotnetRef = DotNetObjectReference.Create(this); + try + { + _module = await JSRuntime.InvokeAsync( + ""import"", ""./shellui-sidebar.js""); + _handlers = await _module.InvokeAsync( + ""initSidebar"", _dotnetRef); + } + catch { /* Pre-render or JS not available */ } + } + } + + [JSInvokable] + public void OnMobileChanged(bool isMobile) + { + if (IsMobile != isMobile) + { + IsMobile = isMobile; + if (isMobile) MobileOpen = false; + StateHasChanged(); + } + } + + [JSInvokable] + public Task OnToggle() + { + Toggle(); + return Task.CompletedTask; + } + + public void Toggle() + { + if (IsMobile) MobileOpen = !MobileOpen; + else IsOpen = !IsOpen; + StateHasChanged(); + } + + public void SetOpen(bool open) + { + if (IsMobile) MobileOpen = open; + else IsOpen = open; + StateHasChanged(); + } + + public void OpenMobileSidebar() { MobileOpen = true; StateHasChanged(); } + public void CloseMobileSidebar() { MobileOpen = false; StateHasChanged(); } + + public async ValueTask DisposeAsync() + { + try + { + if (_handlers != null) + { + await _handlers.InvokeVoidAsync(""dispose""); + await _handlers.DisposeAsync(); + } + if (_module != null) await _module.DisposeAsync(); + } + catch { } + _dotnetRef?.Dispose(); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarRailTemplate.cs b/src/ShellUI.Templates/Templates/SidebarRailTemplate.cs new file mode 100644 index 0000000..6977626 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarRailTemplate.cs @@ -0,0 +1,47 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarRailTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-rail", + DisplayName = "Sidebar Rail", + Description = "Thin clickable rail for collapsing/expanding the sidebar", + Category = ComponentCategory.Layout, + FilePath = "SidebarRail.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private void HandleClick() + { + SidebarProvider?.Toggle(); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarSeparatorTemplate.cs b/src/ShellUI.Templates/Templates/SidebarSeparatorTemplate.cs new file mode 100644 index 0000000..98bc3e0 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarSeparatorTemplate.cs @@ -0,0 +1,29 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarSeparatorTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-separator", + DisplayName = "Sidebar Separator", + Description = "Horizontal divider for sidebar sections", + Category = ComponentCategory.Layout, + FilePath = "SidebarSeparator.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SidebarTemplate.cs b/src/ShellUI.Templates/Templates/SidebarTemplate.cs index 09b64c1..84671b0 100644 --- a/src/ShellUI.Templates/Templates/SidebarTemplate.cs +++ b/src/ShellUI.Templates/Templates/SidebarTemplate.cs @@ -8,33 +8,162 @@ public static class SidebarTemplate { Name = "sidebar", DisplayName = "Sidebar", - Description = "Side navigation panel with toggle", + Description = "Dashboard sidebar with collapsible icon mode, mobile support, and inset variant (shadcn sidebar-08)", Category = ComponentCategory.Layout, FilePath = "Sidebar.razor", - Dependencies = new List() + Dependencies = new List + { + "shell", "sidebar-models", "sidebar-js", "sidebar-provider", + "sidebar-header", "sidebar-content", "sidebar-footer", + "sidebar-group", "sidebar-group-label", "sidebar-group-content", + "sidebar-menu", "sidebar-menu-item", "sidebar-menu-button", + "sidebar-menu-sub", "sidebar-menu-sub-item", "sidebar-menu-sub-button", + "sidebar-menu-action", "sidebar-menu-badge", + "sidebar-separator", "sidebar-trigger", "sidebar-inset", "sidebar-rail" + }, + Tags = new List { "sidebar", "navigation", "layout", "dashboard" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI - +} +else if (SidebarProvider?.IsMobile == true) +{ + @if (SidebarProvider.MobileOpen) + { + +
    -@code { - [Parameter] - public bool IsOpen { get; set; } = true; + +
    +
    + @ChildContent +
    +
    + } +} +else +{ + + +} - [Parameter] - public string ClassName { get; set; } = """"; +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + [Parameter] public SidebarSide Side { get; set; } = SidebarSide.Left; + [Parameter] public SidebarVariant Variant { get; set; } = SidebarVariant.Sidebar; + [Parameter] public SidebarCollapsible Collapsible { get; set; } = SidebarCollapsible.Offcanvas; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + + private bool IsFloatingOrInset => Variant == SidebarVariant.Floating || Variant == SidebarVariant.Inset; + + // Inline styles for CSS variable widths (reliable across Tailwind versions) + private string GapStyle + { + get + { + var state = SidebarProvider?.State ?? ""expanded""; + var isCollapsed = state == ""collapsed""; + + if (isCollapsed && Collapsible == SidebarCollapsible.Offcanvas) + return ""width: 0;""; + if (isCollapsed && Collapsible == SidebarCollapsible.Icon) + return IsFloatingOrInset + ? ""width: calc(var(--sidebar-width-icon) + 1rem);"" + : ""width: var(--sidebar-width-icon);""; + return ""width: var(--sidebar-width);""; + } + } + + private string FixedStyle + { + get + { + var state = SidebarProvider?.State ?? ""expanded""; + var isCollapsed = state == ""collapsed""; + var parts = new List(); + + if (isCollapsed && Collapsible == SidebarCollapsible.Icon) + parts.Add(IsFloatingOrInset + ? ""width: calc(var(--sidebar-width-icon) + 1rem + 2px)"" + : ""width: var(--sidebar-width-icon)""); + else + parts.Add(""width: var(--sidebar-width)""); + + if (Side == SidebarSide.Left) + { + parts.Add(isCollapsed && Collapsible == SidebarCollapsible.Offcanvas + ? ""left: calc(var(--sidebar-width) * -1)"" + : ""left: 0""); + } + else + { + parts.Add(isCollapsed && Collapsible == SidebarCollapsible.Offcanvas + ? ""right: calc(var(--sidebar-width) * -1)"" + : ""right: 0""); + } + + return string.Join(""; "", parts) + "";""; + } + } + + private string GapClass => Shell.Cn( + ""relative h-svh bg-transparent transition-[width] ease-linear duration-200"", + Side == SidebarSide.Right ? ""rotate-180"" : null + ); + + private string FixedClass => Shell.Cn( + ""fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] ease-linear duration-200 md:flex"", + IsFloatingOrInset ? ""p-2"" : null, + !IsFloatingOrInset && Side == SidebarSide.Left ? ""border-r"" : null, + !IsFloatingOrInset && Side == SidebarSide.Right ? ""border-l"" : null, + Class + ); } "; } diff --git a/src/ShellUI.Templates/Templates/SidebarTriggerTemplate.cs b/src/ShellUI.Templates/Templates/SidebarTriggerTemplate.cs new file mode 100644 index 0000000..0b3316e --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarTriggerTemplate.cs @@ -0,0 +1,45 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarTriggerTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar-trigger", + DisplayName = "Sidebar Trigger", + Description = "Toggle button to open/close the sidebar", + Category = ComponentCategory.Layout, + FilePath = "SidebarTrigger.razor", + IsAvailable = false + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [CascadingParameter] public SidebarProvider? SidebarProvider { get; set; } + + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick(MouseEventArgs args) + { + await OnClick.InvokeAsync(args); + SidebarProvider?.Toggle(); + } +} +"; +} From 50e075135eb3152c3545a8e61bfe7c02d12613e7 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 11 Feb 2026 01:18:16 +0200 Subject: [PATCH 03/49] feat: enhance App layout with new navigation links and error handling --- .../Components/Layout/DashboardLayout.razor | 39 ++++++++++ .../Components/Layout/MainLayout.razor | 74 ++++++++++++++----- 2 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 NET9/BlazorInteractiveServer/Components/Layout/DashboardLayout.razor diff --git a/NET9/BlazorInteractiveServer/Components/Layout/DashboardLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/DashboardLayout.razor new file mode 100644 index 0000000..d8b46c4 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Layout/DashboardLayout.razor @@ -0,0 +1,39 @@ +@inherits LayoutComponentBase +@using BlazorInteractiveServer.Components.UI + +@* Dashboard layout — sidebar + content, no navbar/footer *@ + + + + @* ── Shared header: trigger, breadcrumbs, theme toggle ── *@ +
    +
    + + + @HeaderContent +
    +
    + +
    +
    + + @* ── Page content renders here ── *@ +
    + @Body +
    +
    +
    + +
    + An unhandled error has occurred. + Reload + x +
    + +@code { + /// + /// Optional header content (breadcrumbs, page title, etc.) + /// Pages can set this via a cascading parameter or just leave it empty. + /// + [Parameter] public RenderFragment? HeaderContent { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index c6dd46a..bc7f267 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -24,6 +24,8 @@ + +
    An unhandled error has occurred. Reload @@ -150,11 +184,24 @@ { _commandItems = new List { + new CommandItem + { + Title = "Home", + Description = "Go to homepage", + Shortcut = "Ctrl+H", + Action = async () => { NavigationManager.NavigateTo("/"); await Task.CompletedTask; } + }, + new CommandItem + { + Title = "Dashboard", + Description = "View dashboard", + Shortcut = "Ctrl+D", + Action = async () => { NavigationManager.NavigateTo("/dashboard"); await Task.CompletedTask; } + }, new CommandItem { Title = "Documentation", Description = "View ShellUI documentation", - Shortcut = "Ctrl+D", Action = async () => { NavigationManager.NavigateTo("/docs"); await Task.CompletedTask; } }, new CommandItem @@ -180,13 +227,6 @@ Title = "GitHub", Description = "View source code on GitHub", Action = async () => await JSRuntime.InvokeVoidAsync("ShellUI.openUrl", "https://github.com/shellui-dev/shellui", "_blank") - }, - new CommandItem - { - Title = "Home", - Description = "Go to homepage", - Shortcut = "Ctrl+H", - Action = async () => { NavigationManager.NavigateTo("/"); await Task.CompletedTask; } } }; } @@ -213,26 +253,20 @@ private async Task HandleCommandSelected(CommandItem command) { + _isCommandPaletteOpen = false; if (command.Action != null) { await command.Action(); } } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - // Focus the body to enable keyboard events - await JSRuntime.InvokeVoidAsync("ShellUI.focusBody"); - } - } - - private void HandleKeyDown(KeyboardEventArgs e) + private async Task HandleKeyDown(KeyboardEventArgs e) { - if ((e.CtrlKey || e.MetaKey) && string.Equals(e.Key, "k", StringComparison.OrdinalIgnoreCase)) + // Ctrl+K to open command palette + if (e.CtrlKey && e.Key == "k") { - OpenCommandPalette(); + _isCommandPaletteOpen = !_isCommandPaletteOpen; + StateHasChanged(); } } } \ No newline at end of file From c74c88120bf36ce2cfb9f73fdcbf409462358ea8 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 11 Feb 2026 01:19:40 +0200 Subject: [PATCH 04/49] feat: add Sidebar models and JavaScript interop for enhanced sidebar functionality --- .../Models/SidebarModels.cs | 21 ++++++++++ .../wwwroot/shellui-sidebar.js | 38 +++++++++++++++++ src/ShellUI.Templates/ComponentRegistry.cs | 42 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/ShellUI.Components/Models/SidebarModels.cs create mode 100644 src/ShellUI.Components/wwwroot/shellui-sidebar.js diff --git a/src/ShellUI.Components/Models/SidebarModels.cs b/src/ShellUI.Components/Models/SidebarModels.cs new file mode 100644 index 0000000..41dadae --- /dev/null +++ b/src/ShellUI.Components/Models/SidebarModels.cs @@ -0,0 +1,21 @@ +namespace ShellUI.Components; + +public enum SidebarVariant +{ + Sidebar, + Floating, + Inset +} + +public enum SidebarCollapsible +{ + Offcanvas, + Icon, + None +} + +public enum SidebarSide +{ + Left, + Right +} diff --git a/src/ShellUI.Components/wwwroot/shellui-sidebar.js b/src/ShellUI.Components/wwwroot/shellui-sidebar.js new file mode 100644 index 0000000..b5b9f9b --- /dev/null +++ b/src/ShellUI.Components/wwwroot/shellui-sidebar.js @@ -0,0 +1,38 @@ +// ShellUI Sidebar JS Interop Module +// Handles mobile detection, resize events, keyboard shortcuts + +export function initSidebar(dotnetRef) { + const MOBILE_BREAKPOINT = 768; + const checkMobile = () => window.innerWidth < MOBILE_BREAKPOINT; + + // Initial mobile check + dotnetRef.invokeMethodAsync('OnMobileChanged', checkMobile()); + + // Debounced resize handler + let resizeTimer; + const handleResize = () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + dotnetRef.invokeMethodAsync('OnMobileChanged', checkMobile()); + }, 50); + }; + + // Keyboard shortcut: Ctrl/Cmd + B to toggle sidebar + const handleKeydown = (e) => { + if (e.key === 'b' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + dotnetRef.invokeMethodAsync('OnToggle'); + } + }; + + window.addEventListener('resize', handleResize); + document.addEventListener('keydown', handleKeydown); + + return { + dispose: () => { + clearTimeout(resizeTimer); + window.removeEventListener('resize', handleResize); + document.removeEventListener('keydown', handleKeydown); + } + }; +} diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index 3a543d2..654f9e6 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -40,6 +40,27 @@ public static class ComponentRegistry { "tab-models", TabModelsTemplate.Metadata }, { "navbar", NavbarTemplate.Metadata }, { "sidebar", SidebarTemplate.Metadata }, + { "sidebar-models", SidebarModelsTemplate.Metadata }, + { "sidebar-js", SidebarJsTemplate.Metadata }, + { "sidebar-provider", SidebarProviderTemplate.Metadata }, + { "sidebar-header", SidebarHeaderTemplate.Metadata }, + { "sidebar-content", SidebarContentTemplate.Metadata }, + { "sidebar-footer", SidebarFooterTemplate.Metadata }, + { "sidebar-group", SidebarGroupTemplate.Metadata }, + { "sidebar-group-label", SidebarGroupLabelTemplate.Metadata }, + { "sidebar-group-content", SidebarGroupContentTemplate.Metadata }, + { "sidebar-menu", SidebarMenuTemplate.Metadata }, + { "sidebar-menu-item", SidebarMenuItemTemplate.Metadata }, + { "sidebar-menu-button", SidebarMenuButtonTemplate.Metadata }, + { "sidebar-menu-sub", SidebarMenuSubTemplate.Metadata }, + { "sidebar-menu-sub-item", SidebarMenuSubItemTemplate.Metadata }, + { "sidebar-menu-sub-button", SidebarMenuSubButtonTemplate.Metadata }, + { "sidebar-menu-action", SidebarMenuActionTemplate.Metadata }, + { "sidebar-menu-badge", SidebarMenuBadgeTemplate.Metadata }, + { "sidebar-separator", SidebarSeparatorTemplate.Metadata }, + { "sidebar-trigger", SidebarTriggerTemplate.Metadata }, + { "sidebar-inset", SidebarInsetTemplate.Metadata }, + { "sidebar-rail", SidebarRailTemplate.Metadata }, { "dropdown", DropdownTemplate.Metadata }, { "radio-group", RadioGroupTemplate.Metadata }, { "radio-group-item", RadioGroupItemTemplate.Metadata }, @@ -142,6 +163,27 @@ public static class ComponentRegistry "tab-models" => TabModelsTemplate.Content, "navbar" => NavbarTemplate.Content, "sidebar" => SidebarTemplate.Content, + "sidebar-models" => SidebarModelsTemplate.Content, + "sidebar-js" => SidebarJsTemplate.Content, + "sidebar-provider" => SidebarProviderTemplate.Content, + "sidebar-header" => SidebarHeaderTemplate.Content, + "sidebar-content" => SidebarContentTemplate.Content, + "sidebar-footer" => SidebarFooterTemplate.Content, + "sidebar-group" => SidebarGroupTemplate.Content, + "sidebar-group-label" => SidebarGroupLabelTemplate.Content, + "sidebar-group-content" => SidebarGroupContentTemplate.Content, + "sidebar-menu" => SidebarMenuTemplate.Content, + "sidebar-menu-item" => SidebarMenuItemTemplate.Content, + "sidebar-menu-button" => SidebarMenuButtonTemplate.Content, + "sidebar-menu-sub" => SidebarMenuSubTemplate.Content, + "sidebar-menu-sub-item" => SidebarMenuSubItemTemplate.Content, + "sidebar-menu-sub-button" => SidebarMenuSubButtonTemplate.Content, + "sidebar-menu-action" => SidebarMenuActionTemplate.Content, + "sidebar-menu-badge" => SidebarMenuBadgeTemplate.Content, + "sidebar-separator" => SidebarSeparatorTemplate.Content, + "sidebar-trigger" => SidebarTriggerTemplate.Content, + "sidebar-inset" => SidebarInsetTemplate.Content, + "sidebar-rail" => SidebarRailTemplate.Content, "dropdown" => DropdownTemplate.Content, "radio-group" => RadioGroupTemplate.Content, "radio-group-item" => RadioGroupItemTemplate.Content, From a6b0547d02034ea886db68583ad9010fbd38aa85 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 11 Feb 2026 01:20:38 +0200 Subject: [PATCH 05/49] feat: add Dashboard page and enhance App layout with Font Awesome icons --- .../Components/App.razor | 5 +- .../Components/Pages/Dashboard.razor | 55 ++ NET9/BlazorInteractiveServer/wwwroot/app.css | 744 +++++++++++++++--- .../BlazorInteractiveServer/wwwroot/input.css | 200 ++--- .../wwwroot/shellui-sidebar.js | 38 + 5 files changed, 837 insertions(+), 205 deletions(-) create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor create mode 100644 NET9/BlazorInteractiveServer/wwwroot/shellui-sidebar.js diff --git a/NET9/BlazorInteractiveServer/Components/App.razor b/NET9/BlazorInteractiveServer/Components/App.razor index 5080f11..1b4a88a 100644 --- a/NET9/BlazorInteractiveServer/Components/App.razor +++ b/NET9/BlazorInteractiveServer/Components/App.razor @@ -1,4 +1,4 @@ - + @@ -14,6 +14,9 @@ + + + diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor b/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor new file mode 100644 index 0000000..090b0de --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor @@ -0,0 +1,55 @@ +@page "/dashboard" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI + +Dashboard + +@* Stat cards row *@ +
    +
    +
    +

    Total Revenue

    + +
    +
    $45,231.89
    +

    +20.1% from last month

    +
    +
    +
    +

    Subscriptions

    + +
    +
    +2,350
    +

    +180.1% from last month

    +
    +
    +
    +

    Active Now

    + +
    +
    +573
    +

    +201 since last hour

    +
    +
    + +@* Main content area *@ +
    +
    +
    + +
    +
    +

    ShellUI Sidebar — sidebar-08

    +

    + Inset sidebar with secondary navigation, collapsible icon mode, + and full responsive mobile support. +

    +
    +
    + Ctrl+B to collapse + Responsive + Theme Aware +
    +
    +
    diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index 0df1ab0..d78a7b0 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -16,6 +16,11 @@ --tw-leading: initial; --tw-font-weight: initial; --tw-tracking: initial; + --tw-ordinal: initial; + --tw-slashed-zero: initial; + --tw-numeric-figure: initial; + --tw-numeric-spacing: initial; + --tw-numeric-fraction: initial; --tw-shadow: 0 0 #0000; --tw-shadow-color: initial; --tw-shadow-alpha: 100%; @@ -55,6 +60,7 @@ --tw-backdrop-sepia: initial; --tw-duration: initial; --tw-ease: initial; + --tw-content: ""; } } } @@ -110,6 +116,7 @@ --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-tight: -0.025em; + --leading-tight: 1.25; --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --shadow-2xs: var(--shadow-2xs); @@ -281,6 +288,9 @@ .pointer-events-none { pointer-events: none; } + .collapse { + visibility: collapse; + } .sr-only { position: absolute; width: 1px; @@ -319,6 +329,9 @@ .top-0 { top: calc(var(--spacing) * 0); } + .top-1\.5 { + top: calc(var(--spacing) * 1.5); + } .top-1\/2 { top: calc(1/2 * 100%); } @@ -388,6 +401,9 @@ .z-10 { z-index: 10; } + .z-20 { + z-index: 20; + } .z-40 { z-index: 40; } @@ -412,6 +428,12 @@ max-width: 96rem; } } + .mx-2 { + margin-inline: calc(var(--spacing) * 2); + } + .mx-3\.5 { + margin-inline: calc(var(--spacing) * 3.5); + } .mx-auto { margin-inline: auto; } @@ -520,6 +542,14 @@ .aspect-square { aspect-ratio: 1 / 1; } + .size-4 { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + .size-8 { + width: calc(var(--spacing) * 8); + height: calc(var(--spacing) * 8); + } .h-1\.5 { height: calc(var(--spacing) * 1.5); } @@ -598,8 +628,8 @@ .h-px { height: 1px; } - .h-screen { - height: 100vh; + .h-svh { + height: 100svh; } .max-h-60 { max-height: calc(var(--spacing) * 60); @@ -610,12 +640,21 @@ .max-h-screen { max-height: 100vh; } + .min-h-0 { + min-height: calc(var(--spacing) * 0); + } + .min-h-\[50vh\] { + min-height: 50vh; + } .min-h-\[80px\] { min-height: 80px; } .min-h-screen { min-height: 100vh; } + .min-h-svh { + min-height: 100svh; + } .w-1 { width: calc(var(--spacing) * 1); } @@ -700,6 +739,9 @@ .w-\[100px\] { width: 100px; } + .w-auto { + width: auto; + } .w-full { width: 100%; } @@ -727,6 +769,9 @@ .min-w-0 { min-width: calc(var(--spacing) * 0); } + .min-w-5 { + min-width: calc(var(--spacing) * 5); + } .min-w-\[8rem\] { min-width: 8rem; } @@ -752,8 +797,8 @@ --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } - .-translate-x-full { - --tw-translate-x: -100%; + .-translate-x-px { + --tw-translate-x: -1px; translate: var(--tw-translate-x) var(--tw-translate-y); } .translate-x-0 { @@ -768,6 +813,10 @@ --tw-translate-x: -50%; translate: var(--tw-translate-x) var(--tw-translate-y); } + .translate-x-px { + --tw-translate-x: 1px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -779,6 +828,9 @@ .scale-\[1\.02\] { scale: 1.02; } + .rotate-90 { + rotate: 90deg; + } .rotate-180 { rotate: 180deg; } @@ -806,12 +858,18 @@ .touch-none { touch-action: none; } + .resize { + resize: both; + } .list-none { list-style-type: none; } .appearance-none { appearance: none; } + .auto-rows-min { + grid-auto-rows: min-content; + } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -878,6 +936,13 @@ .gap-8 { gap: calc(var(--spacing) * 8); } + .space-y-0 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-1\.5 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -983,6 +1048,9 @@ .rounded-sm { border-radius: calc(var(--radius) - 4px); } + .rounded-xl { + border-radius: calc(var(--radius) + 4px); + } .rounded-t-\[10px\] { border-top-left-radius: 10px; border-top-right-radius: 10px; @@ -995,6 +1063,10 @@ border-style: var(--tw-border-style); border-width: 1px; } + .border-0 { + border-style: var(--tw-border-style); + border-width: 0px; + } .border-2 { border-style: var(--tw-border-style); border-width: 2px; @@ -1062,6 +1134,9 @@ .border-ring { border-color: var(--ring); } + .border-sidebar-border { + border-color: var(--sidebar-border); + } .border-transparent { border-color: transparent; } @@ -1113,6 +1188,9 @@ .bg-card { background-color: var(--card); } + .bg-card\/50 { + background-color: color-mix(in oklab, var(--card) 50%, transparent); + } .bg-destructive { background-color: var(--destructive); } @@ -1149,6 +1227,18 @@ .bg-secondary { background-color: var(--secondary); } + .bg-sidebar { + background-color: var(--sidebar); + } + .bg-sidebar-accent { + background-color: var(--sidebar-accent); + } + .bg-sidebar-border { + background-color: var(--sidebar-border); + } + .bg-sidebar-primary { + background-color: var(--sidebar-primary); + } .bg-transparent { background-color: transparent; } @@ -1251,6 +1341,9 @@ .pr-8 { padding-right: calc(var(--spacing) * 8); } + .pb-2 { + padding-bottom: calc(var(--spacing) * 2); + } .pb-4 { padding-bottom: calc(var(--spacing) * 4); } @@ -1306,6 +1399,10 @@ --tw-leading: 1; line-height: 1; } + .leading-tight { + --tw-leading: var(--leading-tight); + line-height: var(--leading-tight); + } .font-bold { --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); @@ -1371,12 +1468,28 @@ .text-secondary-foreground { color: var(--secondary-foreground); } + .text-sidebar-accent-foreground { + color: var(--sidebar-accent-foreground); + } + .text-sidebar-foreground { + color: var(--sidebar-foreground); + } + .text-sidebar-foreground\/70 { + color: color-mix(in oklab, var(--sidebar-foreground) 70%, transparent); + } + .text-sidebar-primary-foreground { + color: var(--sidebar-primary-foreground); + } .text-white { color: var(--color-white); } .text-yellow-700 { color: var(--color-yellow-700); } + .tabular-nums { + --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,); + } .underline { text-decoration-line: underline; } @@ -1435,6 +1548,9 @@ .ring-ring { --tw-ring-color: var(--ring); } + .ring-sidebar-ring { + --tw-ring-color: var(--sidebar-ring); + } .ring-offset-2 { --tw-ring-offset-width: 2px; --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); @@ -1473,6 +1589,26 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } + .transition-\[left\,right\,width\] { + transition-property: left,right,width; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-\[margin\,opacity\] { + transition-property: margin,opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-\[width\,height\,padding\] { + transition-property: width,height,padding; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-\[width\] { + transition-property: width; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } .transition-all { transition-property: all; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -1505,6 +1641,10 @@ --tw-ease: var(--ease-in-out); transition-timing-function: var(--ease-in-out); } + .ease-linear { + --tw-ease: linear; + transition-timing-function: linear; + } .outline-none { --tw-outline-style: none; outline-style: none; @@ -1513,6 +1653,11 @@ -webkit-user-select: none; user-select: none; } + .group-focus-within\/menu-item\:opacity-100 { + &:is(:where(.group\/menu-item):focus-within *) { + opacity: 100%; + } + } .group-hover\:opacity-100 { &:is(:where(.group):hover *) { @media (hover: hover) { @@ -1520,6 +1665,107 @@ } } } + .group-hover\/menu-item\:opacity-100 { + &:is(:where(.group\/menu-item):hover *) { + @media (hover: hover) { + opacity: 100%; + } + } + } + .group-has-\[\[data-sidebar\=menu-action\]\]\/menu-item\:pr-8 { + &:is(:where(.group\/menu-item):has(*:is([data-sidebar=menu-action])) *) { + padding-right: calc(var(--spacing) * 8); + } + } + .group-data-\[collapsible\=icon\]\:-mt-8 { + &:is(:where(.group)[data-collapsible="icon"] *) { + margin-top: calc(var(--spacing) * -8); + } + } + .group-data-\[collapsible\=icon\]\:hidden { + &:is(:where(.group)[data-collapsible="icon"] *) { + display: none; + } + } + .group-data-\[collapsible\=icon\]\:\!size-8 { + &:is(:where(.group)[data-collapsible="icon"] *) { + width: calc(var(--spacing) * 8) !important; + height: calc(var(--spacing) * 8) !important; + } + } + .group-data-\[collapsible\=icon\]\:overflow-hidden { + &:is(:where(.group)[data-collapsible="icon"] *) { + overflow: hidden; + } + } + .group-data-\[collapsible\=icon\]\:\!p-0 { + &:is(:where(.group)[data-collapsible="icon"] *) { + padding: calc(var(--spacing) * 0) !important; + } + } + .group-data-\[collapsible\=icon\]\:\!p-2 { + &:is(:where(.group)[data-collapsible="icon"] *) { + padding: calc(var(--spacing) * 2) !important; + } + } + .group-data-\[collapsible\=icon\]\:opacity-0 { + &:is(:where(.group)[data-collapsible="icon"] *) { + opacity: 0%; + } + } + .group-data-\[collapsible\=offcanvas\]\:translate-x-0 { + &:is(:where(.group)[data-collapsible="offcanvas"] *) { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + .group-data-\[side\=left\]\:-right-4 { + &:is(:where(.group)[data-side="left"] *) { + right: calc(var(--spacing) * -4); + } + } + .group-data-\[side\=right\]\:left-0 { + &:is(:where(.group)[data-side="right"] *) { + left: calc(var(--spacing) * 0); + } + } + .group-data-\[side\=right\]\:group-data-\[collapsible\=offcanvas\]\:-translate-x-full { + &:is(:where(.group)[data-side="right"] *) { + &:is(:where(.group)[data-collapsible="offcanvas"] *) { + --tw-translate-x: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + } + .group-data-\[variant\=floating\]\:rounded-lg { + &:is(:where(.group)[data-variant="floating"] *) { + border-radius: var(--radius); + } + } + .group-data-\[variant\=floating\]\:border { + &:is(:where(.group)[data-variant="floating"] *) { + border-style: var(--tw-border-style); + border-width: 1px; + } + } + .group-data-\[variant\=floating\]\:border-sidebar-border { + &:is(:where(.group)[data-variant="floating"] *) { + border-color: var(--sidebar-border); + } + } + .group-data-\[variant\=floating\]\:shadow { + &:is(:where(.group)[data-variant="floating"] *) { + --tw-shadow: var(--shadow); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .peer-hover\/menu-button\:text-sidebar-accent-foreground { + &:is(:where(.peer\/menu-button):hover ~ *) { + @media (hover: hover) { + color: var(--sidebar-accent-foreground); + } + } + } .peer-disabled\:cursor-not-allowed { &:is(:where(.peer):disabled ~ *) { cursor: not-allowed; @@ -1530,6 +1776,16 @@ opacity: 70%; } } + .peer-data-\[active\=true\]\/menu-button\:text-sidebar-accent-foreground { + &:is(:where(.peer\/menu-button)[data-active="true"] ~ *) { + color: var(--sidebar-accent-foreground); + } + } + .peer-data-\[variant\=inset\]\:min-h-\[calc\(100svh-1rem\)\] { + &:is(:where(.peer)[data-variant="inset"] ~ *) { + min-height: calc(100svh - 1rem); + } + } .file\:border-0 { &::file-selector-button { border-style: var(--tw-border-style); @@ -1558,6 +1814,44 @@ color: var(--muted-foreground); } } + .after\:absolute { + &::after { + content: var(--tw-content); + position: absolute; + } + } + .after\:-inset-2 { + &::after { + content: var(--tw-content); + inset: calc(var(--spacing) * -2); + } + } + .after\:inset-y-0 { + &::after { + content: var(--tw-content); + inset-block: calc(var(--spacing) * 0); + } + } + .after\:left-1\/2 { + &::after { + content: var(--tw-content); + left: calc(1/2 * 100%); + } + } + .after\:w-\[2px\] { + &::after { + content: var(--tw-content); + width: 2px; + } + } + .group-data-\[collapsible\=offcanvas\]\:after\:left-full { + &:is(:where(.group)[data-collapsible="offcanvas"] *) { + &::after { + content: var(--tw-content); + left: 100%; + } + } + } .hover\:border-primary\/50 { &:hover { @media (hover: hover) { @@ -1666,6 +1960,13 @@ } } } + .hover\:bg-sidebar-accent { + &:hover { + @media (hover: hover) { + background-color: var(--sidebar-accent); + } + } + } .hover\:bg-yellow-600 { &:hover { @media (hover: hover) { @@ -1715,6 +2016,13 @@ } } } + .hover\:text-sidebar-accent-foreground { + &:hover { + @media (hover: hover) { + color: var(--sidebar-accent-foreground); + } + } + } .hover\:underline { &:hover { @media (hover: hover) { @@ -1736,6 +2044,25 @@ } } } + .group-data-\[collapsible\=offcanvas\]\:hover\:bg-sidebar { + &:is(:where(.group)[data-collapsible="offcanvas"] *) { + &:hover { + @media (hover: hover) { + background-color: var(--sidebar); + } + } + } + } + .hover\:after\:bg-sidebar-border { + &:hover { + @media (hover: hover) { + &::after { + content: var(--tw-content); + background-color: var(--sidebar-border); + } + } + } + } .focus\:border-ring { &:focus { border-color: var(--ring); @@ -1828,6 +2155,16 @@ scale: 0.98; } } + .active\:bg-sidebar-accent { + &:active { + background-color: var(--sidebar-accent); + } + } + .active\:text-sidebar-accent-foreground { + &:active { + color: var(--sidebar-accent-foreground); + } + } .disabled\:pointer-events-none { &:disabled { pointer-events: none; @@ -1843,6 +2180,37 @@ opacity: 50%; } } + .has-\[\[data-variant\=inset\]\]\:bg-sidebar { + &:has(*:is([data-variant=inset])) { + background-color: var(--sidebar); + } + } + .aria-disabled\:pointer-events-none { + &[aria-disabled="true"] { + pointer-events: none; + } + } + .aria-disabled\:opacity-50 { + &[aria-disabled="true"] { + opacity: 50%; + } + } + .data-\[active\=true\]\:bg-sidebar-accent { + &[data-active="true"] { + background-color: var(--sidebar-accent); + } + } + .data-\[active\=true\]\:font-medium { + &[data-active="true"] { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + } + .data-\[active\=true\]\:text-sidebar-accent-foreground { + &[data-active="true"] { + color: var(--sidebar-accent-foreground); + } + } .data-\[disabled\]\:pointer-events-none { &[data-disabled] { pointer-events: none; @@ -1874,12 +2242,35 @@ color: var(--muted-foreground); } } + .data-\[state\=open\]\:opacity-100 { + &[data-state="open"] { + opacity: 100%; + } + } .data-\[state\=open\]\:duration-500 { &[data-state="open"] { --tw-duration: 500ms; transition-duration: 500ms; } } + .data-\[state\=open\]\:hover\:bg-sidebar-accent { + &[data-state="open"] { + &:hover { + @media (hover: hover) { + background-color: var(--sidebar-accent); + } + } + } + } + .data-\[state\=open\]\:hover\:text-sidebar-accent-foreground { + &[data-state="open"] { + &:hover { + @media (hover: hover) { + color: var(--sidebar-accent-foreground); + } + } + } + } .data-\[state\=selected\]\:bg-muted { &[data-state="selected"] { background-color: var(--muted); @@ -1895,6 +2286,11 @@ display: block; } } + .sm\:flex { + @media (width >= 40rem) { + display: flex; + } + } .sm\:inline-flex { @media (width >= 40rem) { display: inline-flex; @@ -1991,11 +2387,26 @@ line-height: var(--tw-leading, var(--text-xl--line-height)); } } + .md\:block { + @media (width >= 48rem) { + display: block; + } + } + .md\:flex { + @media (width >= 48rem) { + display: flex; + } + } .md\:inline-flex { @media (width >= 48rem) { display: inline-flex; } } + .md\:min-h-min { + @media (width >= 48rem) { + min-height: min-content; + } + } .md\:max-w-\[420px\] { @media (width >= 48rem) { max-width: 420px; @@ -2022,6 +2433,48 @@ line-height: var(--tw-leading, var(--text-4xl--line-height)); } } + .md\:opacity-0 { + @media (width >= 48rem) { + opacity: 0%; + } + } + .md\:peer-data-\[variant\=inset\]\:m-2 { + @media (width >= 48rem) { + &:is(:where(.peer)[data-variant="inset"] ~ *) { + margin: calc(var(--spacing) * 2); + } + } + } + .md\:peer-data-\[variant\=inset\]\:ml-0 { + @media (width >= 48rem) { + &:is(:where(.peer)[data-variant="inset"] ~ *) { + margin-left: calc(var(--spacing) * 0); + } + } + } + .md\:peer-data-\[variant\=inset\]\:rounded-xl { + @media (width >= 48rem) { + &:is(:where(.peer)[data-variant="inset"] ~ *) { + border-radius: calc(var(--radius) + 4px); + } + } + } + .md\:peer-data-\[variant\=inset\]\:shadow { + @media (width >= 48rem) { + &:is(:where(.peer)[data-variant="inset"] ~ *) { + --tw-shadow: var(--shadow); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + .after\:md\:hidden { + &::after { + content: var(--tw-content); + @media (width >= 48rem) { + display: none; + } + } + } .lg\:flex { @media (width >= 64rem) { display: flex; @@ -2221,6 +2674,28 @@ padding-right: calc(var(--spacing) * 0); } } + .\[\&\>i\]\:w-4 { + &>i { + width: calc(var(--spacing) * 4); + } + } + .\[\&\>i\]\:shrink-0 { + &>i { + flex-shrink: 0; + } + } + .\[\&\>i\]\:text-center { + &>i { + text-align: center; + } + } + .\[\&\>span\:last-child\]\:truncate { + &>span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } .\[\&\>svg\]\:absolute { &>svg { position: absolute; @@ -2236,6 +2711,17 @@ left: calc(var(--spacing) * 4); } } + .\[\&\>svg\]\:size-4 { + &>svg { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + } + .\[\&\>svg\]\:shrink-0 { + &>svg { + flex-shrink: 0; + } + } .\[\&\>svg\]\:text-blue-600 { &>svg { color: var(--color-blue-600); @@ -2256,6 +2742,11 @@ color: var(--color-green-600); } } + .\[\&\>svg\]\:text-sidebar-accent-foreground { + &>svg { + color: var(--sidebar-accent-foreground); + } + } .\[\&\>svg\]\:text-yellow-600 { &>svg { color: var(--color-yellow-600); @@ -2272,112 +2763,132 @@ padding-left: calc(var(--spacing) * 7); } } + .\[\[data-side\=left\]_\&\]\:cursor-w-resize { + [data-side=left] & { + cursor: w-resize; + } + } + .\[\[data-side\=left\]\[data-state\=collapsed\]_\&\]\:cursor-e-resize { + [data-side=left][data-state=collapsed] & { + cursor: e-resize; + } + } + .\[\[data-side\=right\]_\&\]\:cursor-e-resize { + [data-side=right] & { + cursor: e-resize; + } + } + .\[\[data-side\=right\]\[data-state\=collapsed\]_\&\]\:cursor-w-resize { + [data-side=right][data-state=collapsed] & { + cursor: w-resize; + } + } } :root { - --background: oklch(1 0 0); - --foreground: oklch(0.1450 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.1450 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.1450 0 0); - --primary: oklch(0.2050 0 0); - --primary-foreground: oklch(0.9850 0 0); - --secondary: oklch(0.9700 0 0); - --secondary-foreground: oklch(0.2050 0 0); - --muted: oklch(0.9700 0 0); - --muted-foreground: oklch(0.5560 0 0); - --accent: oklch(0.9700 0 0); - --accent-foreground: oklch(0.2050 0 0); - --destructive: oklch(0.5770 0.2450 27.3250); - --destructive-foreground: oklch(1 0 0); - --border: oklch(0.9220 0 0); - --input: oklch(0.9220 0 0); - --ring: oklch(0.7080 0 0); - --chart-1: oklch(0.8100 0.1000 252); - --chart-2: oklch(0.6200 0.1900 260); - --chart-3: oklch(0.5500 0.2200 263); - --chart-4: oklch(0.4900 0.2200 264); - --chart-5: oklch(0.4200 0.1800 266); - --sidebar: oklch(0.9850 0 0); - --sidebar-foreground: oklch(0.1450 0 0); - --sidebar-primary: oklch(0.2050 0 0); - --sidebar-primary-foreground: oklch(0.9850 0 0); - --sidebar-accent: oklch(0.9700 0 0); - --sidebar-accent-foreground: oklch(0.2050 0 0); - --sidebar-border: oklch(0.9220 0 0); - --sidebar-ring: oklch(0.7080 0 0); - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --radius: 0.625rem; - --shadow-x: 0; - --shadow-y: 1px; - --shadow-blur: 3px; - --shadow-spread: 0px; - --shadow-opacity: 0.1; - --shadow-color: oklch(0 0 0); - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(0.9689 0.0090 314.7819); + --foreground: oklch(0.3729 0.0306 259.7328); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.3729 0.0306 259.7328); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.3729 0.0306 259.7328); + --primary: oklch(0.7090 0.1592 293.5412); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9073 0.0530 306.0902); + --secondary-foreground: oklch(0.4461 0.0263 256.8018); + --muted: oklch(0.9464 0.0327 307.1745); + --muted-foreground: oklch(0.5510 0.0234 264.3637); + --accent: oklch(0.9376 0.0260 321.9388); + --accent-foreground: oklch(0.3729 0.0306 259.7328); + --destructive: oklch(0.8077 0.1035 19.5706); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.9073 0.0530 306.0902); + --input: oklch(0.9073 0.0530 306.0902); + --ring: oklch(0.7090 0.1592 293.5412); + --chart-1: oklch(0.7090 0.1592 293.5412); + --chart-2: oklch(0.6056 0.2189 292.7172); + --chart-3: oklch(0.5413 0.2466 293.0090); + --chart-4: oklch(0.4907 0.2412 292.5809); + --chart-5: oklch(0.4320 0.2106 292.7591); + --sidebar: oklch(0.9073 0.0530 306.0902); + --sidebar-foreground: oklch(0.3729 0.0306 259.7328); + --sidebar-primary: oklch(0.7090 0.1592 293.5412); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9376 0.0260 321.9388); + --sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328); + --sidebar-border: oklch(0.9073 0.0530 306.0902); + --sidebar-ring: oklch(0.7090 0.1592 293.5412); + --font-sans: Kode Mono, ui-monospace, monospace; + --font-serif: Source Serif 4, serif; + --font-mono: IBM Plex Mono, monospace; + --radius: 1.5rem; + --shadow-x: 0px; + --shadow-y: 8px; + --shadow-blur: 16px; + --shadow-spread: -4px; + --shadow-opacity: 0.08; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-sm: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow-md: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 2px 4px -5px hsl(0 0% 0% / 0.08); + --shadow-lg: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 4px 6px -5px hsl(0 0% 0% / 0.08); + --shadow-xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 8px 10px -5px hsl(0 0% 0% / 0.08); + --shadow-2xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.20); --tracking-normal: 0em; --spacing: 0.25rem; } .dark { - --background: oklch(0.1450 0 0); - --foreground: oklch(0.9850 0 0); - --card: oklch(0.2050 0 0); - --card-foreground: oklch(0.9850 0 0); - --popover: oklch(0.2690 0 0); - --popover-foreground: oklch(0.9850 0 0); - --primary: oklch(0.9220 0 0); - --primary-foreground: oklch(0.2050 0 0); - --secondary: oklch(0.2690 0 0); - --secondary-foreground: oklch(0.9850 0 0); - --muted: oklch(0.2690 0 0); - --muted-foreground: oklch(0.7080 0 0); - --accent: oklch(0.3710 0 0); - --accent-foreground: oklch(0.9850 0 0); - --destructive: oklch(0.7040 0.1910 22.2160); - --destructive-foreground: oklch(0.9850 0 0); - --border: oklch(0.2750 0 0); - --input: oklch(0.3250 0 0); - --ring: oklch(0.5560 0 0); - --chart-1: oklch(0.8100 0.1000 252); - --chart-2: oklch(0.6200 0.1900 260); - --chart-3: oklch(0.5500 0.2200 263); - --chart-4: oklch(0.4900 0.2200 264); - --chart-5: oklch(0.4200 0.1800 266); - --sidebar: oklch(0.2050 0 0); - --sidebar-foreground: oklch(0.9850 0 0); - --sidebar-primary: oklch(0.4880 0.2430 264.3760); - --sidebar-primary-foreground: oklch(0.9850 0 0); - --sidebar-accent: oklch(0.2690 0 0); - --sidebar-accent-foreground: oklch(0.9850 0 0); - --sidebar-border: oklch(0.2750 0 0); - --sidebar-ring: oklch(0.4390 0 0); - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --radius: 0.625rem; - --shadow-x: 0; - --shadow-y: 1px; - --shadow-blur: 3px; - --shadow-spread: 0px; - --shadow-opacity: 0.1; - --shadow-color: oklch(0 0 0); - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(0.2161 0.0061 56.0434); + --foreground: oklch(0.9299 0.0334 272.7879); + --card: oklch(0.2805 0.0309 307.2326); + --card-foreground: oklch(0.9299 0.0334 272.7879); + --popover: oklch(0.2805 0.0309 307.2326); + --popover-foreground: oklch(0.9299 0.0334 272.7879); + --primary: oklch(0.7874 0.1179 295.7538); + --primary-foreground: oklch(0.2161 0.0061 56.0434); + --secondary: oklch(0.3416 0.0444 308.8496); + --secondary-foreground: oklch(0.8717 0.0093 258.3382); + --muted: oklch(0.2283 0.0375 302.9006); + --muted-foreground: oklch(0.7137 0.0192 261.3246); + --accent: oklch(0.3858 0.0509 304.6383); + --accent-foreground: oklch(0.8717 0.0093 258.3382); + --destructive: oklch(0.8077 0.1035 19.5706); + --destructive-foreground: oklch(0.2161 0.0061 56.0434); + --border: oklch(0.3416 0.0444 308.8496); + --input: oklch(0.3416 0.0444 308.8496); + --ring: oklch(0.7874 0.1179 295.7538); + --chart-1: oklch(0.7874 0.1179 295.7538); + --chart-2: oklch(0.7090 0.1592 293.5412); + --chart-3: oklch(0.6056 0.2189 292.7172); + --chart-4: oklch(0.5413 0.2466 293.0090); + --chart-5: oklch(0.4907 0.2412 292.5809); + --sidebar: oklch(0.3416 0.0444 308.8496); + --sidebar-foreground: oklch(0.9299 0.0334 272.7879); + --sidebar-primary: oklch(0.7874 0.1179 295.7538); + --sidebar-primary-foreground: oklch(0.2161 0.0061 56.0434); + --sidebar-accent: oklch(0.3858 0.0509 304.6383); + --sidebar-accent-foreground: oklch(0.8717 0.0093 258.3382); + --sidebar-border: oklch(0.3416 0.0444 308.8496); + --sidebar-ring: oklch(0.7874 0.1179 295.7538); + --font-sans: Kode Mono, ui-monospace, monospace; + --font-serif: Source Serif 4, serif; + --font-mono: IBM Plex Mono, monospace; + --radius: 1.5rem; + --shadow-x: 0px; + --shadow-y: 8px; + --shadow-blur: 16px; + --shadow-spread: -4px; + --shadow-opacity: 0.08; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-sm: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow-md: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 2px 4px -5px hsl(0 0% 0% / 0.08); + --shadow-lg: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 4px 6px -5px hsl(0 0% 0% / 0.08); + --shadow-xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 8px 10px -5px hsl(0 0% 0% / 0.08); + --shadow-2xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.20); } html, body, :host { font-family: var(--font-sans) !important; @@ -2674,6 +3185,26 @@ html, body, :host { syntax: "*"; inherits: false; } +@property --tw-ordinal { + syntax: "*"; + inherits: false; +} +@property --tw-slashed-zero { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-figure { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-spacing { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-fraction { + syntax: "*"; + inherits: false; +} @property --tw-shadow { syntax: "*"; inherits: false; @@ -2841,6 +3372,11 @@ html, body, :host { syntax: "*"; inherits: false; } +@property --tw-content { + syntax: "*"; + initial-value: ""; + inherits: false; +} @keyframes spin { to { transform: rotate(360deg); diff --git a/NET9/BlazorInteractiveServer/wwwroot/input.css b/NET9/BlazorInteractiveServer/wwwroot/input.css index 76fe35f..3db0e4f 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/input.css +++ b/NET9/BlazorInteractiveServer/wwwroot/input.css @@ -1,111 +1,111 @@ @import "tailwindcss"; :root { - --background: oklch(1 0 0); - --foreground: oklch(0.1450 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.1450 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.1450 0 0); - --primary: oklch(0.2050 0 0); - --primary-foreground: oklch(0.9850 0 0); - --secondary: oklch(0.9700 0 0); - --secondary-foreground: oklch(0.2050 0 0); - --muted: oklch(0.9700 0 0); - --muted-foreground: oklch(0.5560 0 0); - --accent: oklch(0.9700 0 0); - --accent-foreground: oklch(0.2050 0 0); - --destructive: oklch(0.5770 0.2450 27.3250); - --destructive-foreground: oklch(1 0 0); - --border: oklch(0.9220 0 0); - --input: oklch(0.9220 0 0); - --ring: oklch(0.7080 0 0); - --chart-1: oklch(0.8100 0.1000 252); - --chart-2: oklch(0.6200 0.1900 260); - --chart-3: oklch(0.5500 0.2200 263); - --chart-4: oklch(0.4900 0.2200 264); - --chart-5: oklch(0.4200 0.1800 266); - --sidebar: oklch(0.9850 0 0); - --sidebar-foreground: oklch(0.1450 0 0); - --sidebar-primary: oklch(0.2050 0 0); - --sidebar-primary-foreground: oklch(0.9850 0 0); - --sidebar-accent: oklch(0.9700 0 0); - --sidebar-accent-foreground: oklch(0.2050 0 0); - --sidebar-border: oklch(0.9220 0 0); - --sidebar-ring: oklch(0.7080 0 0); - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --radius: 0.625rem; - --shadow-x: 0; - --shadow-y: 1px; - --shadow-blur: 3px; - --shadow-spread: 0px; - --shadow-opacity: 0.1; - --shadow-color: oklch(0 0 0); - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(0.9689 0.0090 314.7819); + --foreground: oklch(0.3729 0.0306 259.7328); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.3729 0.0306 259.7328); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.3729 0.0306 259.7328); + --primary: oklch(0.7090 0.1592 293.5412); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9073 0.0530 306.0902); + --secondary-foreground: oklch(0.4461 0.0263 256.8018); + --muted: oklch(0.9464 0.0327 307.1745); + --muted-foreground: oklch(0.5510 0.0234 264.3637); + --accent: oklch(0.9376 0.0260 321.9388); + --accent-foreground: oklch(0.3729 0.0306 259.7328); + --destructive: oklch(0.8077 0.1035 19.5706); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.9073 0.0530 306.0902); + --input: oklch(0.9073 0.0530 306.0902); + --ring: oklch(0.7090 0.1592 293.5412); + --chart-1: oklch(0.7090 0.1592 293.5412); + --chart-2: oklch(0.6056 0.2189 292.7172); + --chart-3: oklch(0.5413 0.2466 293.0090); + --chart-4: oklch(0.4907 0.2412 292.5809); + --chart-5: oklch(0.4320 0.2106 292.7591); + --sidebar: oklch(0.9073 0.0530 306.0902); + --sidebar-foreground: oklch(0.3729 0.0306 259.7328); + --sidebar-primary: oklch(0.7090 0.1592 293.5412); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9376 0.0260 321.9388); + --sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328); + --sidebar-border: oklch(0.9073 0.0530 306.0902); + --sidebar-ring: oklch(0.7090 0.1592 293.5412); + --font-sans: Kode Mono, ui-monospace, monospace; + --font-serif: Source Serif 4, serif; + --font-mono: IBM Plex Mono, monospace; + --radius: 1.5rem; + --shadow-x: 0px; + --shadow-y: 8px; + --shadow-blur: 16px; + --shadow-spread: -4px; + --shadow-opacity: 0.08; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-sm: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow-md: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 2px 4px -5px hsl(0 0% 0% / 0.08); + --shadow-lg: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 4px 6px -5px hsl(0 0% 0% / 0.08); + --shadow-xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 8px 10px -5px hsl(0 0% 0% / 0.08); + --shadow-2xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.20); --tracking-normal: 0em; --spacing: 0.25rem; } .dark { - --background: oklch(0.1450 0 0); - --foreground: oklch(0.9850 0 0); - --card: oklch(0.2050 0 0); - --card-foreground: oklch(0.9850 0 0); - --popover: oklch(0.2690 0 0); - --popover-foreground: oklch(0.9850 0 0); - --primary: oklch(0.9220 0 0); - --primary-foreground: oklch(0.2050 0 0); - --secondary: oklch(0.2690 0 0); - --secondary-foreground: oklch(0.9850 0 0); - --muted: oklch(0.2690 0 0); - --muted-foreground: oklch(0.7080 0 0); - --accent: oklch(0.3710 0 0); - --accent-foreground: oklch(0.9850 0 0); - --destructive: oklch(0.7040 0.1910 22.2160); - --destructive-foreground: oklch(0.9850 0 0); - --border: oklch(0.2750 0 0); - --input: oklch(0.3250 0 0); - --ring: oklch(0.5560 0 0); - --chart-1: oklch(0.8100 0.1000 252); - --chart-2: oklch(0.6200 0.1900 260); - --chart-3: oklch(0.5500 0.2200 263); - --chart-4: oklch(0.4900 0.2200 264); - --chart-5: oklch(0.4200 0.1800 266); - --sidebar: oklch(0.2050 0 0); - --sidebar-foreground: oklch(0.9850 0 0); - --sidebar-primary: oklch(0.4880 0.2430 264.3760); - --sidebar-primary-foreground: oklch(0.9850 0 0); - --sidebar-accent: oklch(0.2690 0 0); - --sidebar-accent-foreground: oklch(0.9850 0 0); - --sidebar-border: oklch(0.2750 0 0); - --sidebar-ring: oklch(0.4390 0 0); - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --radius: 0.625rem; - --shadow-x: 0; - --shadow-y: 1px; - --shadow-blur: 3px; - --shadow-spread: 0px; - --shadow-opacity: 0.1; - --shadow-color: oklch(0 0 0); - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(0.2161 0.0061 56.0434); + --foreground: oklch(0.9299 0.0334 272.7879); + --card: oklch(0.2805 0.0309 307.2326); + --card-foreground: oklch(0.9299 0.0334 272.7879); + --popover: oklch(0.2805 0.0309 307.2326); + --popover-foreground: oklch(0.9299 0.0334 272.7879); + --primary: oklch(0.7874 0.1179 295.7538); + --primary-foreground: oklch(0.2161 0.0061 56.0434); + --secondary: oklch(0.3416 0.0444 308.8496); + --secondary-foreground: oklch(0.8717 0.0093 258.3382); + --muted: oklch(0.2283 0.0375 302.9006); + --muted-foreground: oklch(0.7137 0.0192 261.3246); + --accent: oklch(0.3858 0.0509 304.6383); + --accent-foreground: oklch(0.8717 0.0093 258.3382); + --destructive: oklch(0.8077 0.1035 19.5706); + --destructive-foreground: oklch(0.2161 0.0061 56.0434); + --border: oklch(0.3416 0.0444 308.8496); + --input: oklch(0.3416 0.0444 308.8496); + --ring: oklch(0.7874 0.1179 295.7538); + --chart-1: oklch(0.7874 0.1179 295.7538); + --chart-2: oklch(0.7090 0.1592 293.5412); + --chart-3: oklch(0.6056 0.2189 292.7172); + --chart-4: oklch(0.5413 0.2466 293.0090); + --chart-5: oklch(0.4907 0.2412 292.5809); + --sidebar: oklch(0.3416 0.0444 308.8496); + --sidebar-foreground: oklch(0.9299 0.0334 272.7879); + --sidebar-primary: oklch(0.7874 0.1179 295.7538); + --sidebar-primary-foreground: oklch(0.2161 0.0061 56.0434); + --sidebar-accent: oklch(0.3858 0.0509 304.6383); + --sidebar-accent-foreground: oklch(0.8717 0.0093 258.3382); + --sidebar-border: oklch(0.3416 0.0444 308.8496); + --sidebar-ring: oklch(0.7874 0.1179 295.7538); + --font-sans: Kode Mono, ui-monospace, monospace; + --font-serif: Source Serif 4, serif; + --font-mono: IBM Plex Mono, monospace; + --radius: 1.5rem; + --shadow-x: 0px; + --shadow-y: 8px; + --shadow-blur: 16px; + --shadow-spread: -4px; + --shadow-opacity: 0.08; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); + --shadow-sm: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); + --shadow-md: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 2px 4px -5px hsl(0 0% 0% / 0.08); + --shadow-lg: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 4px 6px -5px hsl(0 0% 0% / 0.08); + --shadow-xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 8px 10px -5px hsl(0 0% 0% / 0.08); + --shadow-2xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.20); } @theme inline { diff --git a/NET9/BlazorInteractiveServer/wwwroot/shellui-sidebar.js b/NET9/BlazorInteractiveServer/wwwroot/shellui-sidebar.js new file mode 100644 index 0000000..b5b9f9b --- /dev/null +++ b/NET9/BlazorInteractiveServer/wwwroot/shellui-sidebar.js @@ -0,0 +1,38 @@ +// ShellUI Sidebar JS Interop Module +// Handles mobile detection, resize events, keyboard shortcuts + +export function initSidebar(dotnetRef) { + const MOBILE_BREAKPOINT = 768; + const checkMobile = () => window.innerWidth < MOBILE_BREAKPOINT; + + // Initial mobile check + dotnetRef.invokeMethodAsync('OnMobileChanged', checkMobile()); + + // Debounced resize handler + let resizeTimer; + const handleResize = () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + dotnetRef.invokeMethodAsync('OnMobileChanged', checkMobile()); + }, 50); + }; + + // Keyboard shortcut: Ctrl/Cmd + B to toggle sidebar + const handleKeydown = (e) => { + if (e.key === 'b' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + dotnetRef.invokeMethodAsync('OnToggle'); + } + }; + + window.addEventListener('resize', handleResize); + document.addEventListener('keydown', handleKeydown); + + return { + dispose: () => { + clearTimeout(resizeTimer); + window.removeEventListener('resize', handleResize); + document.removeEventListener('keydown', handleKeydown); + } + }; +} From 8e6b3dac5052233cd8c17ba1adf949dd30a75fb3 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 00:48:28 +0200 Subject: [PATCH 06/49] feat: enhance Dashboard page with new chart components for data visualization --- .../Components/Pages/Dashboard.razor | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor b/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor index 090b0de..a90c78a 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor @@ -2,10 +2,13 @@ @layout BlazorInteractiveServer.Components.Layout.DashboardLayout @rendermode InteractiveServer @using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components.UI.Variants +@using BlazorInteractiveServer.Components.Models +@using ApexCharts Dashboard -@* Stat cards row *@ +@* Stat cards with charts *@
    @@ -14,6 +17,11 @@
    $45,231.89

    +20.1% from last month

    +
    + +
    @@ -22,6 +30,11 @@
    +2,350

    +180.1% from last month

    +
    + +
    @@ -30,6 +43,11 @@
    +573

    +201 since last hour

    +
    + +
    @@ -53,3 +71,40 @@
    + +@code { + private List RevenueData { get; set; } = new(); + private List SubscriptionData { get; set; } = new(); + private List ActiveData { get; set; } = new(); + + protected override void OnInitialized() + { + RevenueData = new List + { + new() { Month = "Jan", Amount = 38210 }, + new() { Month = "Feb", Amount = 40120 }, + new() { Month = "Mar", Amount = 38900 }, + new() { Month = "Apr", Amount = 42350 }, + new() { Month = "May", Amount = 43890 }, + new() { Month = "Jun", Amount = 45231 } + }; + SubscriptionData = new List + { + new() { Month = "Jan", Amount = 1850 }, + new() { Month = "Feb", Amount = 1920 }, + new() { Month = "Mar", Amount = 2010 }, + new() { Month = "Apr", Amount = 2150 }, + new() { Month = "May", Amount = 2240 }, + new() { Month = "Jun", Amount = 2350 } + }; + ActiveData = new List + { + new() { Month = "Jan", Amount = 420 }, + new() { Month = "Feb", Amount = 458 }, + new() { Month = "Mar", Amount = 491 }, + new() { Month = "Apr", Amount = 512 }, + new() { Month = "May", Amount = 538 }, + new() { Month = "Jun", Amount = 573 } + }; + } +} From cf053bc13b6011780dfeff194ff3b017474d0b15 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 00:48:48 +0200 Subject: [PATCH 07/49] feat: update Home page layout to use DashboardLayout for improved structure --- NET9/BlazorInteractiveServer/Components/Pages/Home.razor | 1 + 1 file changed, 1 insertion(+) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor index 041f098..f487e8e 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor @@ -1,4 +1,5 @@ @page "/" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout @using BlazorInteractiveServer.Components.UI @using BlazorInteractiveServer.Components.Models @using BlazorInteractiveServer.Components.Demo From e6927d07ac6e00371954dd1acb663b7544df1d1f Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 00:49:28 +0200 Subject: [PATCH 08/49] feat: add new utility classes and Blazor error UI styles for improved layout and error handling --- NET9/BlazorInteractiveServer/wwwroot/app.css | 32 +++++++++++++++++++ .../BlazorInteractiveServer/wwwroot/input.css | 25 +++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index d78a7b0..c18916e 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -428,6 +428,9 @@ max-width: 96rem; } } + .-mx-2 { + margin-inline: calc(var(--spacing) * -2); + } .mx-2 { margin-inline: calc(var(--spacing) * 2); } @@ -449,6 +452,9 @@ .mt-2 { margin-top: calc(var(--spacing) * 2); } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } .mt-4 { margin-top: calc(var(--spacing) * 4); } @@ -619,6 +625,9 @@ .h-\[1px\] { height: 1px; } + .h-\[80px\] { + height: 80px; + } .h-auto { height: auto; } @@ -3118,6 +3127,29 @@ html, body, :host { font-family: var(--font-sans) !important; font-weight: 500 !important; } +#blazor-error-ui { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + background: var(--destructive); + color: var(--destructive-foreground); + padding: 1rem; + z-index: 9999; + text-align: center; +} +#blazor-error-ui.show { + display: block; +} +#blazor-error-ui a { + color: inherit; + text-decoration: underline; + margin: 0 0.5rem; +} +#blazor-error-ui a:hover { + opacity: 0.8; +} @property --tw-translate-x { syntax: "*"; inherits: false; diff --git a/NET9/BlazorInteractiveServer/wwwroot/input.css b/NET9/BlazorInteractiveServer/wwwroot/input.css index 3db0e4f..b91ca11 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/input.css +++ b/NET9/BlazorInteractiveServer/wwwroot/input.css @@ -421,3 +421,28 @@ html, body, :host { font-family: var(--font-sans) !important; font-weight: 500 !important; } + +/* Blazor error UI - hidden by default, shown only when Blazor adds .show on error */ +#blazor-error-ui { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + background: var(--destructive); + color: var(--destructive-foreground); + padding: 1rem; + z-index: 9999; + text-align: center; +} +#blazor-error-ui.show { + display: block; +} +#blazor-error-ui a { + color: inherit; + text-decoration: underline; + margin: 0 0.5rem; +} +#blazor-error-ui a:hover { + opacity: 0.8; +} From 2509fb5693de74a2ca3142644cc095588781c20a Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 00:55:26 +0200 Subject: [PATCH 09/49] feat: add ChartStylesTemplate for ApexCharts integration with ShellUI theme --- .../Templates/ChartStylesTemplate.cs | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/ShellUI.Templates/Templates/ChartStylesTemplate.cs diff --git a/src/ShellUI.Templates/Templates/ChartStylesTemplate.cs b/src/ShellUI.Templates/Templates/ChartStylesTemplate.cs new file mode 100644 index 0000000..5f054c7 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ChartStylesTemplate.cs @@ -0,0 +1,263 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class ChartStylesTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "chart-styles", + DisplayName = "Chart Styles", + Description = "CSS styles for ApexCharts integration with ShellUI theme system", + Category = ComponentCategory.DataDisplay, + FilePath = "wwwroot/css/charts.css", + IsAvailable = false, + Dependencies = new List(), + Variants = new List(), + Tags = new List { "chart", "css", "styles", "apexcharts" } + }; + + public static string Content => @"/* ========================================================================== + ShellUI Chart Styles - ApexCharts Theme Integration + Auto-installed by: shellui add chart + Add to your App.razor or _Host.cshtml: + + ========================================================================== */ + +/* Canvas */ +.apexcharts-canvas { + background: transparent !important; +} + +/* Tooltip - Uses ShellUI theme variables with fallbacks */ +.apexcharts-tooltip { + z-index: 9999 !important; + position: absolute !important; + background: var(--popover, #fff) !important; + border: 1px solid var(--border, #e5e7eb) !important; + border-radius: 8px !important; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1) !important; + padding: 6px 8px !important; + font-family: inherit !important; + font-size: 11px !important; + line-height: 1.4 !important; + color: var(--popover-foreground, #09090b) !important; +} + +/* Backdrop blur with browser support fallback */ +@supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) { + .apexcharts-tooltip { + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + } +} + +/* Tooltip title */ +.apexcharts-tooltip-title { + background: var(--muted, #f4f4f5) !important; + border-bottom: 1px solid var(--border, #e5e7eb) !important; + color: var(--foreground, #09090b) !important; + font-family: inherit !important; + font-weight: 600 !important; + font-size: 11px !important; + padding: 4px 6px !important; + margin: -6px -8px 4px -8px !important; +} + +/* Custom Tooltip - Full control over structure */ +.custom-tooltip { + padding: 0 !important; + display: flex !important; + flex-direction: column !important; + gap: 6px !important; +} + +.custom-tooltip-title { + font-size: 12px !important; + font-weight: 600 !important; + color: var(--foreground, #09090b) !important; + line-height: 1.2 !important; + padding-bottom: 4px !important; + border-bottom: 1px solid var(--border, #e5e7eb) !important; + margin-bottom: 2px !important; +} + +.custom-tooltip-item { + display: flex !important; + align-items: center !important; + gap: 8px !important; + padding: 0 !important; + line-height: 1 !important; +} + +.custom-tooltip-marker { + width: 8px !important; + height: 8px !important; + border-radius: 2px !important; + flex-shrink: 0 !important; + display: block !important; +} + +.custom-tooltip-label { + font-size: 12px !important; + font-weight: 500 !important; + color: var(--muted-foreground, #71717a) !important; + line-height: 1 !important; +} + +.custom-tooltip-value { + font-size: 12px !important; + font-weight: 600 !important; + color: var(--popover-foreground, #09090b) !important; + line-height: 1 !important; + margin-left: 4px !important; +} + +/* Fallback for default ApexCharts tooltip */ +.apexcharts-tooltip-series-group { + padding: 3px 0 !important; + display: grid !important; + grid-template-columns: 8px 1fr !important; + align-items: center !important; + gap: 8px !important; +} + +.apexcharts-tooltip-marker { + width: 8px !important; + height: 8px !important; + border-radius: 2px !important; + margin: 0 !important; + padding: 0 !important; +} + +.apexcharts-tooltip-text { + font-family: inherit !important; + font-size: 11px !important; + color: var(--popover-foreground, #09090b) !important; + padding: 0 !important; + margin: 0 !important; + line-height: 1.1 !important; +} + +.apexcharts-tooltip-text-y-label, +.apexcharts-tooltip-text-y-value { + line-height: 1.1 !important; +} + +.apexcharts-tooltip-text-y-label { + font-weight: 500 !important; + color: var(--muted-foreground, #71717a) !important; +} + +.apexcharts-tooltip-text-y-value { + font-weight: 600 !important; + color: var(--popover-foreground, #09090b) !important; + margin-left: 4px !important; +} + +/* Axis Tooltips */ +.apexcharts-xaxistooltip, +.apexcharts-yaxistooltip { + z-index: 9998 !important; + background: var(--popover, #fff) !important; + border: 1px solid var(--border, #e5e7eb) !important; + border-radius: 4px !important; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1) !important; + font-family: inherit !important; + font-size: 11px !important; + color: var(--popover-foreground, #09090b) !important; + padding: 4px 8px !important; +} + +.apexcharts-xaxistooltip-bottom:before, +.apexcharts-xaxistooltip-bottom:after { + border-bottom-color: var(--border, #e5e7eb) !important; +} + +/* Legend */ +.apexcharts-legend { + padding: 8px 0 4px 0 !important; +} + +.apexcharts-legend-series { + display: inline-flex !important; + align-items: center !important; + margin: 0 8px 4px 0 !important; + gap: 6px !important; +} + +.apexcharts-legend-text { + color: var(--foreground, #09090b) !important; + font-family: inherit !important; + font-size: 13px !important; + font-weight: 500 !important; +} + +.apexcharts-legend-marker { + border-radius: 2px !important; + flex-shrink: 0 !important; +} + +/* Toolbar */ +.apexcharts-toolbar { + z-index: 100 !important; +} + +.apexcharts-menu { + z-index: 9999 !important; + background: var(--popover, #fff) !important; + border: 1px solid var(--border, #e5e7eb) !important; + border-radius: 8px !important; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1) !important; + padding: 4px !important; +} + +.apexcharts-menu-item { + color: var(--foreground, #09090b) !important; + font-family: inherit !important; + font-size: 13px !important; + padding: 6px 12px !important; + border-radius: 4px !important; + transition: background-color 150ms !important; +} + +.apexcharts-menu-item:hover { + background: var(--accent, #f4f4f5) !important; + color: var(--accent-foreground, #18181b) !important; +} + +.apexcharts-toolbar-item { + color: var(--muted-foreground, #71717a) !important; + transition: color 150ms !important; +} + +.apexcharts-toolbar-item:hover { + color: var(--foreground, #09090b) !important; +} + +/* Grid and Axis */ +.apexcharts-gridline { + stroke: var(--border, #e5e7eb) !important; + stroke-opacity: 0.5 !important; +} + +.apexcharts-text { + fill: var(--muted-foreground, #71717a) !important; + font-family: inherit !important; +} + +.apexcharts-xaxis-label, +.apexcharts-yaxis-label { + fill: var(--muted-foreground, #71717a) !important; +} + +/* Data Labels */ +.apexcharts-datalabel, +.apexcharts-datalabel-label, +.apexcharts-datalabel-value { + fill: var(--foreground, #09090b) !important; + font-family: inherit !important; + font-weight: 500 !important; +} +"; +} From 9d2c140244c942994de07cf0a484bdbf6edb166d Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 03:09:44 +0200 Subject: [PATCH 10/49] feat: enhance Dashboard and AppSidebar components for improved layout and user experience --- .../Components/Pages/Dashboard.razor | 35 ++- .../Components/UI/AppSidebar.razor | 215 ++++++------------ .../Components/UI/SidebarMenuButton.razor | 2 +- .../BlazorInteractiveServer/wwwroot/input.css | 203 +++++++++-------- .../Components/AppSidebar.razor | 61 ++--- .../Components/SidebarMenuButton.razor | 2 +- 6 files changed, 222 insertions(+), 296 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor b/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor index a90c78a..1803b00 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Dashboard.razor @@ -8,45 +8,42 @@ Dashboard -@* Stat cards with charts *@ +@* Stat cards with charts — label + value on top row, taller charts *@
    -
    +

    Total Revenue

    - + $45,231.89
    -
    $45,231.89
    -

    +20.1% from last month

    -
    +

    +20.1% from last month

    +
    + Height="160px" Theme="ChartTheme.Default" />
    -
    +

    Subscriptions

    - + +2,350
    -
    +2,350
    -

    +180.1% from last month

    -
    +

    +180.1% from last month

    +
    + Height="160px" Theme="ChartTheme.Default" />
    -
    +

    Active Now

    - + +573
    -
    +573
    -

    +201 since last hour

    -
    +

    +201 since last hour

    +
    + Height="160px" Theme="ChartTheme.Colorful" />
    diff --git a/NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor b/NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor index 5b112c8..14a849f 100644 --- a/NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor +++ b/NET9/BlazorInteractiveServer/Components/UI/AppSidebar.razor @@ -1,38 +1,32 @@ @namespace BlazorInteractiveServer.Components.UI +@inject NavigationManager Navigation @* ────────────────────────────────────────────────────────── - AppSidebar — shadcn sidebar-08 (Inset + Secondary Nav) + AppSidebar — Inset sidebar with secondary nav (shadcn sidebar-08) - Usage: + SIMPLE USAGE (3 pieces): -
    -
    - - - ... -
    -
    -
    - -
    +
    ... ...
    +
    @Body or your content
    + + Collapsible: Offcanvas (default) = sidebar slides off when collapsed. + Set Collapsible="SidebarCollapsible.Icon" for icon strip when collapsed. ────────────────────────────────────────────────────────── *@ - + @* ── Header: Logo & Company ── *@ - -
    - -
    -
    - Acme Inc - Enterprise + + +
    + ShellUI + Components
    @@ -42,68 +36,35 @@ @* ── Content ── *@ - @* ─── NavMain: Platform ─── *@ + @* ─── Dashboard ─── *@ - Platform + Navigation - @foreach (var item in _navMainItems) - { - - - - @item.Title - - @if (item.SubItems?.Count > 0) - { - - - - @if (item.IsExpanded) - { - - @foreach (var sub in item.SubItems) - { - - - @sub.Title - - - } - - } - } - - } + + + + Dashboard + + - @* ─── NavProjects ─── *@ + @* ─── Components (like shellui.dev/docs) ─── *@ - Projects + Components - @foreach (var project in _projects) + @foreach (var item in _componentItems) { - - - @project.Name + + + @item.Title - - - More - } - - - - More - - @@ -137,13 +98,22 @@ @onclick:stopPropagation="true" data-sidebar="menu-button" data-size="lg" - class="peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2"> - -
    + title="@_user.Name" + class="peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:!justify-center group-data-[collapsible=icon]:!items-center"> + @* Icon + expandable arrows when collapsed *@ + + @* Avatar + text when expanded *@ + + + +
    @_user.Name @_user.Email
    - + @if (_showUserMenu) @@ -192,6 +162,8 @@ @code { + /// Offcanvas = sidebar slides off when collapsed (simpler). Icon = shows icon strip when collapsed. + [Parameter] public SidebarCollapsible Collapsible { get; set; } = SidebarCollapsible.Offcanvas; [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } @@ -201,6 +173,15 @@ private void ToggleUserMenu() => _showUserMenu = !_showUserMenu; private void ToggleExpand(NavItem item) { item.IsExpanded = !item.IsExpanded; } + private bool IsCurrentPath(string path) + { + var uri = new Uri(Navigation.Uri); + var currentPath = uri.AbsolutePath.TrimEnd('/'); + var targetPath = path.TrimEnd('/'); + return currentPath.Equals(targetPath, StringComparison.OrdinalIgnoreCase) + || currentPath.StartsWith(targetPath + "/", StringComparison.OrdinalIgnoreCase); + } + // ── Data Models ── private class NavItem { @@ -236,88 +217,22 @@ // ── User ── private readonly UserProfile _user = new() { - Name = "shadcn", - Email = "m@example.com", - Initials = "CN" - }; - - // ── Navigation Data ── - private readonly List _navMainItems = new() - { - new NavItem - { - Title = "Playground", - Url = "#", - IsActive = true, - IsExpanded = true, - IconClass = "fa-solid fa-code", - SubItems = new() - { - new SubNavItem { Title = "History", Url = "#" }, - new SubNavItem { Title = "Starred", Url = "#" }, - new SubNavItem { Title = "Settings", Url = "#" } - } - }, - new NavItem - { - Title = "Models", - Url = "#", - IconClass = "fa-solid fa-microchip", - SubItems = new() - { - new SubNavItem { Title = "Genesis", Url = "#" }, - new SubNavItem { Title = "Explorer", Url = "#" }, - new SubNavItem { Title = "Quantum", Url = "#" } - } - }, - new NavItem - { - Title = "Documentation", - Url = "#", - IconClass = "fa-solid fa-book", - SubItems = new() - { - new SubNavItem { Title = "Introduction", Url = "#" }, - new SubNavItem { Title = "Get Started", Url = "#" }, - new SubNavItem { Title = "Tutorials", Url = "#" }, - new SubNavItem { Title = "Changelog", Url = "#" } - } - }, - new NavItem - { - Title = "Settings", - Url = "#", - IconClass = "fa-solid fa-gear", - SubItems = new() - { - new SubNavItem { Title = "General", Url = "#" }, - new SubNavItem { Title = "Team", Url = "#" }, - new SubNavItem { Title = "Billing", Url = "#" }, - new SubNavItem { Title = "Limits", Url = "#" } - } - } + Name = "Shewart", + Email = "shewart@shellui.dev", + Initials = "SH" }; - private readonly List _projects = new() + // ── Component links (like shellui.dev/docs) ── + private readonly List _componentItems = new() { - new ProjectItem - { - Name = "Design Engineering", - Url = "#", - IconClass = "fa-solid fa-pen-ruler" - }, - new ProjectItem - { - Name = "Sales & Marketing", - Url = "#", - IconClass = "fa-solid fa-bullhorn" - }, - new ProjectItem - { - Name = "Travel", - Url = "#", - IconClass = "fa-solid fa-location-dot" - } + new NavItem { Title = "Overview", Url = "/components", IconClass = "fa-solid fa-layer-group" }, + new NavItem { Title = "Button", Url = "/components/form/button", IconClass = "fa-solid fa-square" }, + new NavItem { Title = "Checkbox", Url = "/components/form/checkbox", IconClass = "fa-solid fa-square-check" }, + new NavItem { Title = "Input", Url = "/components/form/input", IconClass = "fa-solid fa-keyboard" }, + new NavItem { Title = "Select", Url = "/components/form/select", IconClass = "fa-solid fa-caret-down" }, + new NavItem { Title = "Switch", Url = "/components/form/switch", IconClass = "fa-solid fa-toggle-on" }, + new NavItem { Title = "Badge", Url = "/components/data-display/badge", IconClass = "fa-solid fa-tag" }, + new NavItem { Title = "Card", Url = "/components/data-display/card", IconClass = "fa-solid fa-id-card" } }; private readonly List _secondaryItems = new() diff --git a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor index 6e429e3..17cf4f3 100644 --- a/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor +++ b/NET9/BlazorInteractiveServer/Components/UI/SidebarMenuButton.razor @@ -9,7 +9,7 @@ }; var buttonClass = Shell.Cn( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:!justify-center group-data-[collapsible=icon]:[&>span]:!hidden [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center", sizeClass, Class ); diff --git a/NET9/BlazorInteractiveServer/wwwroot/input.css b/NET9/BlazorInteractiveServer/wwwroot/input.css index b91ca11..ae1408f 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/input.css +++ b/NET9/BlazorInteractiveServer/wwwroot/input.css @@ -1,111 +1,113 @@ @import "tailwindcss"; +@custom-variant dark (&:is(.dark *)); + :root { - --background: oklch(0.9689 0.0090 314.7819); - --foreground: oklch(0.3729 0.0306 259.7328); - --card: oklch(1.0000 0 0); - --card-foreground: oklch(0.3729 0.0306 259.7328); - --popover: oklch(1.0000 0 0); - --popover-foreground: oklch(0.3729 0.0306 259.7328); - --primary: oklch(0.7090 0.1592 293.5412); - --primary-foreground: oklch(1.0000 0 0); - --secondary: oklch(0.9073 0.0530 306.0902); - --secondary-foreground: oklch(0.4461 0.0263 256.8018); - --muted: oklch(0.9464 0.0327 307.1745); - --muted-foreground: oklch(0.5510 0.0234 264.3637); - --accent: oklch(0.9376 0.0260 321.9388); - --accent-foreground: oklch(0.3729 0.0306 259.7328); - --destructive: oklch(0.8077 0.1035 19.5706); - --destructive-foreground: oklch(1.0000 0 0); - --border: oklch(0.9073 0.0530 306.0902); - --input: oklch(0.9073 0.0530 306.0902); - --ring: oklch(0.7090 0.1592 293.5412); - --chart-1: oklch(0.7090 0.1592 293.5412); - --chart-2: oklch(0.6056 0.2189 292.7172); - --chart-3: oklch(0.5413 0.2466 293.0090); - --chart-4: oklch(0.4907 0.2412 292.5809); - --chart-5: oklch(0.4320 0.2106 292.7591); - --sidebar: oklch(0.9073 0.0530 306.0902); - --sidebar-foreground: oklch(0.3729 0.0306 259.7328); - --sidebar-primary: oklch(0.7090 0.1592 293.5412); - --sidebar-primary-foreground: oklch(1.0000 0 0); - --sidebar-accent: oklch(0.9376 0.0260 321.9388); - --sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328); - --sidebar-border: oklch(0.9073 0.0530 306.0902); - --sidebar-ring: oklch(0.7090 0.1592 293.5412); - --font-sans: Kode Mono, ui-monospace, monospace; - --font-serif: Source Serif 4, serif; - --font-mono: IBM Plex Mono, monospace; - --radius: 1.5rem; + --background: oklch(0.9900 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --popover: oklch(0.9900 0 0); + --popover-foreground: oklch(0 0 0); + --primary: oklch(0 0 0); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.9400 0 0); + --secondary-foreground: oklch(0 0 0); + --muted: oklch(0.9700 0 0); + --muted-foreground: oklch(0.4400 0 0); + --accent: oklch(0.9400 0 0); + --accent-foreground: oklch(0 0 0); + --destructive: oklch(0.6300 0.1900 23.0300); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.9200 0 0); + --input: oklch(0.9400 0 0); + --ring: oklch(0 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5500 0.2200 264.5300); + --chart-3: oklch(0.7200 0 0); + --chart-4: oklch(0.9200 0 0); + --chart-5: oklch(0.5600 0 0); + --sidebar: oklch(0.9900 0 0); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0 0 0); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.9400 0 0); + --sidebar-accent-foreground: oklch(0 0 0); + --sidebar-border: oklch(0.9400 0 0); + --sidebar-ring: oklch(0 0 0); + --font-sans: Geist, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Geist Mono, monospace; + --radius: 0.5rem; --shadow-x: 0px; - --shadow-y: 8px; - --shadow-blur: 16px; - --shadow-spread: -4px; - --shadow-opacity: 0.08; + --shadow-y: 1px; + --shadow-blur: 2px; + --shadow-spread: 0px; + --shadow-opacity: 0.18; --shadow-color: hsl(0 0% 0%); - --shadow-2xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); - --shadow-xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); - --shadow-sm: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); - --shadow: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); - --shadow-md: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 2px 4px -5px hsl(0 0% 0% / 0.08); - --shadow-lg: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 4px 6px -5px hsl(0 0% 0% / 0.08); - --shadow-xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 8px 10px -5px hsl(0 0% 0% / 0.08); - --shadow-2xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.20); + --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18); + --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18); + --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18); + --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); --tracking-normal: 0em; --spacing: 0.25rem; } .dark { - --background: oklch(0.2161 0.0061 56.0434); - --foreground: oklch(0.9299 0.0334 272.7879); - --card: oklch(0.2805 0.0309 307.2326); - --card-foreground: oklch(0.9299 0.0334 272.7879); - --popover: oklch(0.2805 0.0309 307.2326); - --popover-foreground: oklch(0.9299 0.0334 272.7879); - --primary: oklch(0.7874 0.1179 295.7538); - --primary-foreground: oklch(0.2161 0.0061 56.0434); - --secondary: oklch(0.3416 0.0444 308.8496); - --secondary-foreground: oklch(0.8717 0.0093 258.3382); - --muted: oklch(0.2283 0.0375 302.9006); - --muted-foreground: oklch(0.7137 0.0192 261.3246); - --accent: oklch(0.3858 0.0509 304.6383); - --accent-foreground: oklch(0.8717 0.0093 258.3382); - --destructive: oklch(0.8077 0.1035 19.5706); - --destructive-foreground: oklch(0.2161 0.0061 56.0434); - --border: oklch(0.3416 0.0444 308.8496); - --input: oklch(0.3416 0.0444 308.8496); - --ring: oklch(0.7874 0.1179 295.7538); - --chart-1: oklch(0.7874 0.1179 295.7538); - --chart-2: oklch(0.7090 0.1592 293.5412); - --chart-3: oklch(0.6056 0.2189 292.7172); - --chart-4: oklch(0.5413 0.2466 293.0090); - --chart-5: oklch(0.4907 0.2412 292.5809); - --sidebar: oklch(0.3416 0.0444 308.8496); - --sidebar-foreground: oklch(0.9299 0.0334 272.7879); - --sidebar-primary: oklch(0.7874 0.1179 295.7538); - --sidebar-primary-foreground: oklch(0.2161 0.0061 56.0434); - --sidebar-accent: oklch(0.3858 0.0509 304.6383); - --sidebar-accent-foreground: oklch(0.8717 0.0093 258.3382); - --sidebar-border: oklch(0.3416 0.0444 308.8496); - --sidebar-ring: oklch(0.7874 0.1179 295.7538); - --font-sans: Kode Mono, ui-monospace, monospace; - --font-serif: Source Serif 4, serif; - --font-mono: IBM Plex Mono, monospace; - --radius: 1.5rem; + --background: oklch(0 0 0); + --foreground: oklch(1 0 0); + --card: oklch(0.1400 0 0); + --card-foreground: oklch(1 0 0); + --popover: oklch(0.1800 0 0); + --popover-foreground: oklch(1 0 0); + --primary: oklch(1 0 0); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.2500 0 0); + --secondary-foreground: oklch(1 0 0); + --muted: oklch(0.2300 0 0); + --muted-foreground: oklch(0.7200 0 0); + --accent: oklch(0.3200 0 0); + --accent-foreground: oklch(1 0 0); + --destructive: oklch(0.6900 0.2000 23.9100); + --destructive-foreground: oklch(0 0 0); + --border: oklch(0.2600 0 0); + --input: oklch(0.3200 0 0); + --ring: oklch(0.7200 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5800 0.2100 260.8400); + --chart-3: oklch(0.5600 0 0); + --chart-4: oklch(0.4400 0 0); + --chart-5: oklch(0.9200 0 0); + --sidebar: oklch(0.1800 0 0); + --sidebar-foreground: oklch(1 0 0); + --sidebar-primary: oklch(1 0 0); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0.3200 0 0); + --sidebar-accent-foreground: oklch(1 0 0); + --sidebar-border: oklch(0.3200 0 0); + --sidebar-ring: oklch(0.7200 0 0); + --font-sans: Geist, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Geist Mono, monospace; + --radius: 0.5rem; --shadow-x: 0px; - --shadow-y: 8px; - --shadow-blur: 16px; - --shadow-spread: -4px; - --shadow-opacity: 0.08; + --shadow-y: 1px; + --shadow-blur: 2px; + --shadow-spread: 0px; + --shadow-opacity: 0.18; --shadow-color: hsl(0 0% 0%); - --shadow-2xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); - --shadow-xs: 0px 8px 16px -4px hsl(0 0% 0% / 0.04); - --shadow-sm: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); - --shadow: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 1px 2px -5px hsl(0 0% 0% / 0.08); - --shadow-md: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 2px 4px -5px hsl(0 0% 0% / 0.08); - --shadow-lg: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 4px 6px -5px hsl(0 0% 0% / 0.08); - --shadow-xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.08), 0px 8px 10px -5px hsl(0 0% 0% / 0.08); - --shadow-2xl: 0px 8px 16px -4px hsl(0 0% 0% / 0.20); + --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18); + --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18); + --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18); + --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); } @theme inline { @@ -160,6 +162,15 @@ --shadow-xl: var(--shadow-xl); --shadow-2xl: var(--shadow-2xl); } + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} /* Ensure font-family updates when theme changes - must come after @theme inline */ html, body, :host { font-family: var(--font-sans) !important; diff --git a/src/ShellUI.Components/Components/AppSidebar.razor b/src/ShellUI.Components/Components/AppSidebar.razor index 70e279e..5a34e17 100644 --- a/src/ShellUI.Components/Components/AppSidebar.razor +++ b/src/ShellUI.Components/Components/AppSidebar.razor @@ -1,38 +1,31 @@ @namespace ShellUI.Components @* ────────────────────────────────────────────────────────── - AppSidebar — shadcn sidebar-08 (Inset + Secondary Nav) + AppSidebar — Inset sidebar with secondary nav (shadcn sidebar-08) - Usage: + SIMPLE USAGE (3 pieces): -
    -
    - - - ... -
    -
    -
    - -
    +
    ... ...
    +
    @Body or your content
    + + Collapsible: Offcanvas (default) = sidebar slides off when collapsed. + Set Collapsible="SidebarCollapsible.Icon" for icon strip when collapsed. ────────────────────────────────────────────────────────── *@ - + @* ── Header: Logo & Company ── *@ - -
    - -
    -
    - Acme Inc - Enterprise + + +
    + ShellUI + Components
    @@ -137,13 +130,21 @@ @onclick:stopPropagation="true" data-sidebar="menu-button" data-size="lg" - class="peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2"> - -
    - @_user.Name - @_user.Email + title="Shewart" + class="peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:!justify-center group-data-[collapsible=icon]:!items-center"> + @* Icon + expandable arrows when collapsed *@ + +
    + +
    + @_user.Name + @_user.Email +
    +
    - @if (_showUserMenu) @@ -192,6 +193,8 @@ @code { + /// Offcanvas = sidebar slides off when collapsed (simpler). Icon = shows icon strip when collapsed. + [Parameter] public SidebarCollapsible Collapsible { get; set; } = SidebarCollapsible.Offcanvas; [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } @@ -236,9 +239,9 @@ // ── User ── private readonly UserProfile _user = new() { - Name = "shadcn", - Email = "m@example.com", - Initials = "CN" + Name = "Shewart", + Email = "shewart@shellui.dev", + Initials = "SH" }; // ── Navigation Data ── diff --git a/src/ShellUI.Components/Components/SidebarMenuButton.razor b/src/ShellUI.Components/Components/SidebarMenuButton.razor index b271890..0d2fdec 100644 --- a/src/ShellUI.Components/Components/SidebarMenuButton.razor +++ b/src/ShellUI.Components/Components/SidebarMenuButton.razor @@ -9,7 +9,7 @@ }; var buttonClass = Shell.Cn( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:[&>span]:hidden [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center", sizeClass, Class ); From cb10594b1e8058bb20665f61f464f704a819bac2 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 03:10:02 +0200 Subject: [PATCH 11/49] feat: add GitHub Actions workflow for deploying ShellUI Preview to GitHub Pages --- .github/workflows/preview-pages.yml | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/preview-pages.yml diff --git a/.github/workflows/preview-pages.yml b/.github/workflows/preview-pages.yml new file mode 100644 index 0000000..028b40c --- /dev/null +++ b/.github/workflows/preview-pages.yml @@ -0,0 +1,60 @@ +# Deploy ShellUI Preview to GitHub Pages (for docs embedding) +name: Deploy Preview to GitHub Pages + +on: + push: + branches: [ main ] + paths: + - 'NET9/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: '9.0.x' + + - name: Publish Preview (Blazor WASM) + run: | + dotnet publish NET9/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 + + - 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 From e79ff68673548ca482a2f84079c8dd840ce75b5a Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 23:52:07 +0200 Subject: [PATCH 12/49] feat: update MainLayout to enhance sidebar navigation and overall layout structure --- NET9/ShellUI.Preview/Layout/MainLayout.razor | 26 +++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/NET9/ShellUI.Preview/Layout/MainLayout.razor b/NET9/ShellUI.Preview/Layout/MainLayout.razor index 76eb725..41ebcea 100644 --- a/NET9/ShellUI.Preview/Layout/MainLayout.razor +++ b/NET9/ShellUI.Preview/Layout/MainLayout.razor @@ -1,16 +1,14 @@ -@inherits LayoutComponentBase -
    - - -
    -
    - About -
    - -
    - @Body -
    +@inherits LayoutComponentBase +
    + +
    + @Body
    From e1d30393a65830bd0d6f983e10fba546dfc41a5e Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 23:54:05 +0200 Subject: [PATCH 13/49] feat: update component path resolution in ComponentInstaller to support layout blocks Modified the path resolution logic in ComponentInstaller to differentiate between standard components and layout blocks, ensuring correct installation paths based on the component type. --- src/ShellUI.CLI/Services/ComponentInstaller.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs index ccb94c6..aee3f14 100644 --- a/src/ShellUI.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -147,7 +147,10 @@ private static InstallResult InstallComponentInternal(string componentName, Shel return InstallResult.Failed; } - var componentPath = Path.Combine(Directory.GetCurrentDirectory(), config.ComponentsPath, metadata.FilePath); + var basePath = metadata.IsLayoutBlock + ? Path.Combine(Directory.GetCurrentDirectory(), config.LayoutPath ?? "Components/Layout") + : Path.Combine(Directory.GetCurrentDirectory(), config.ComponentsPath); + var componentPath = Path.GetFullPath(Path.Combine(basePath, metadata.FilePath)); // Check if already exists if (File.Exists(componentPath) && !force) From 297095c3c874795e337f36b9277059aacd29dfe4 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 23:54:54 +0200 Subject: [PATCH 14/49] feat: add LayoutPath to InitService for improved component organization Introduced a new LayoutPath property in InitService to specify the path for layout components, enhancing the structure and organization of the component installation process. --- src/ShellUI.CLI/Services/InitService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs index be32479..183d7e8 100644 --- a/src/ShellUI.CLI/Services/InitService.cs +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -102,6 +102,7 @@ await AnsiConsole.Status() { Style = style, ComponentsPath = "Components/UI", + LayoutPath = "Components/Layout", ProjectType = projectInfo.ProjectType, Tailwind = new TailwindConfig { From 7d70dffd555be7585c670e0d8213403ff6b52803 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 8 Mar 2026 23:58:37 +0200 Subject: [PATCH 15/49] feat: add LayoutPath property to ShellUIConfig for enhanced component organization --- src/ShellUI.Core/Models/ShellUIConfig.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ShellUI.Core/Models/ShellUIConfig.cs b/src/ShellUI.Core/Models/ShellUIConfig.cs index 3c5cfea..af4ce1c 100644 --- a/src/ShellUI.Core/Models/ShellUIConfig.cs +++ b/src/ShellUI.Core/Models/ShellUIConfig.cs @@ -5,6 +5,7 @@ public class ShellUIConfig public string Schema { get; set; } = "https://shellui.dev/schema.json"; public string Style { get; set; } = "default"; // default, new-york, minimal public string ComponentsPath { get; set; } = "Components/UI"; + public string LayoutPath { get; set; } = "Components/Layout"; public TailwindConfig Tailwind { get; set; } = new(); public List InstalledComponents { get; set; } = new(); public ProjectType ProjectType { get; set; } From 10a7a12f916abdaafadae802c93662dd289cacb0 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 11 Mar 2026 23:33:38 +0200 Subject: [PATCH 16/49] feat: add documentation for new data display components including Accordion, Alert, Avatar, Badge, Card, Progress, and Tabs --- .../Components/DataDisplay/AccordionDoc.razor | 74 ++++++++++++++++ .../Components/DataDisplay/AlertDoc.razor | 64 ++++++++++++++ .../Components/DataDisplay/AvatarDoc.razor | 61 +++++++++++++ .../Components/DataDisplay/BadgeDoc.razor | 63 +++++++++++++ .../Components/DataDisplay/CardDoc.razor | 73 +++++++++++++++ .../Components/DataDisplay/ProgressDoc.razor | 62 +++++++++++++ .../Components/DataDisplay/TabsDoc.razor | 88 +++++++++++++++++++ 7 files changed, 485 insertions(+) create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AccordionDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AlertDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AvatarDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/BadgeDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/CardDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/ProgressDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/TabsDoc.razor diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AccordionDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AccordionDoc.razor new file mode 100644 index 0000000..b92240e --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AccordionDoc.razor @@ -0,0 +1,74 @@ +@page "/components/data-display/accordion" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Accordion | ShellUI + +
    +
    +

    Accordion

    +

    Collapsible content panels with single or multiple open items.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add accordion + +
    +
    + +
    +

    Usage

    + +

    Single (default)

    +
    + + +

    Yes. It adheres to the WAI-ARIA design pattern.

    +
    + +

    Yes. It comes with default styles that match the rest of the UI.

    +
    + +

    Yes. It's animated by default with smooth transitions.

    +
    +
    +
    + +

    Multiple

    +
    + + +

    Content for item 1.

    +
    + +

    Content for item 2.

    +
    +
    +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    TypeAccordionTypeSingleSingle or Multiple open items
    CollapsiblebooltrueAllow closing the last open item
    AccordionItem TitlestringPanel header text
    +
    +
    +
    diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AlertDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AlertDoc.razor new file mode 100644 index 0000000..9cd6e34 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AlertDoc.razor @@ -0,0 +1,64 @@ +@page "/components/data-display/alert" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Alert | ShellUI + +
    +
    +

    Alert

    +

    Displays a callout for user attention with variant styles.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add alert + +
    +
    + +
    +

    Usage

    + +

    Variants

    +
    + + This is an informational alert. + + + Operation completed successfully! + + + Please check your input. + + + Something went wrong. Please try again. + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    VariantAlertVariantDefaultVisual style
    Titlestring""Alert title
    IconRenderFragmentnullOptional icon
    ChildContentRenderFragmentAlert content
    +
    +
    +
    diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AvatarDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AvatarDoc.razor new file mode 100644 index 0000000..601bc12 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/AvatarDoc.razor @@ -0,0 +1,61 @@ +@page "/components/data-display/avatar" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Avatar | ShellUI + +
    +
    +

    Avatar

    +

    An image or fallback for representing a user or entity.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add avatar + +
    +
    + +
    +

    Usage

    + +

    With Fallback

    +
    + + + +
    + +

    Sizes

    +
    + + + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    Srcstring?nullImage URL
    Fallbackstring?nullFallback text (e.g. initials)
    SizeAvatarSizeDefaultSm, Default, Lg
    +
    +
    +
    diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/BadgeDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/BadgeDoc.razor new file mode 100644 index 0000000..9d87f3a --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/BadgeDoc.razor @@ -0,0 +1,63 @@ +@page "/components/data-display/badge" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Badge | ShellUI + +
    +
    +

    Badge

    +

    Small status indicator or label component.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add badge + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    + Default +
    + +

    Variants

    +
    + Default + Secondary + Destructive + Outline + Success + Warning + Info +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    VariantBadgeVariantDefaultVisual style
    Classstring?nullAdditional CSS classes
    ChildContentRenderFragmentBadge content
    +
    +
    +
    diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/CardDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/CardDoc.razor new file mode 100644 index 0000000..b3b3d75 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/CardDoc.razor @@ -0,0 +1,73 @@ +@page "/components/data-display/card" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Card | ShellUI + +
    +
    +

    Card

    +

    Container component for grouping related content.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add card + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    + + + Card Title + Card description goes here. + + +

    This is the main content of the card. It can contain any content you want.

    +
    + + + +
    +
    + +

    Simple Card

    +
    + + +

    Cards can be used with just CardContent for simpler layouts.

    +
    +
    +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + + +
    ComponentDescription
    CardRoot container
    CardHeaderHeader section
    CardTitleTitle text
    CardDescriptionDescription text
    CardContentMain content area
    CardFooterFooter section
    +
    +
    +
    diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/ProgressDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/ProgressDoc.razor new file mode 100644 index 0000000..7f0ce8a --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/ProgressDoc.razor @@ -0,0 +1,62 @@ +@page "/components/data-display/progress" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Progress | ShellUI + +
    +
    +

    Progress

    +

    Displays an indicator showing the completion progress of a task.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add progress + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    Valueint0Progress value (0-100)
    Heightstring"0.5rem"Bar height
    +
    +
    +
    + +@code { + private int progressValue = 60; +} diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/TabsDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/TabsDoc.razor new file mode 100644 index 0000000..38f3907 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/DataDisplay/TabsDoc.razor @@ -0,0 +1,88 @@ +@page "/components/data-display/tabs" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components.Models + +Tabs | ShellUI + +
    +
    +

    Tabs

    +

    Organize content into switchable panels.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add tabs + +
    +
    + +
    +

    Usage

    + +

    Compositional (TabsList + TabsTrigger + TabsContent)

    +
    + + + Overview + Analytics + Reports + + +
    +

    Overview content goes here.

    +
    +
    + +
    +

    Analytics content goes here.

    +
    +
    + +
    +

    Reports content goes here.

    +
    +
    +
    +
    + +

    With TabItems (programmatic)

    +
    + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + +
    ComponentDescription
    TabsRoot container
    TabsListTab button container
    TabsTriggerIndividual tab button
    TabsContentTab panel content
    +
    +
    +
    + +@code { + private string activeTab = "overview"; + private string activeTab2 = "tab1"; + + private List tabItems = new() + { + new TabItem { Id = "tab1", Label = "Tab 1", Content = builder => builder.AddMarkupContent(0, "

    Tab 1 content

    ") }, + new TabItem { Id = "tab2", Label = "Tab 2", Content = builder => builder.AddMarkupContent(0, "

    Tab 2 content

    ") } + }; +} From 123ab1259d0d3d488c9ce8022237099e2e3df863 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 11 Mar 2026 23:34:14 +0200 Subject: [PATCH 17/49] feat: add documentation for form components including Button, Checkbox, Input, Label, Select, Switch, Textarea --- .../Pages/Components/Form/ButtonDoc.razor | 101 ++++++++++++++++++ .../Pages/Components/Form/CheckboxDoc.razor | 89 +++++++++++++++ .../Pages/Components/Form/InputDoc.razor | 78 ++++++++++++++ .../Pages/Components/Form/LabelDoc.razor | 61 +++++++++++ .../Pages/Components/Form/SelectDoc.razor | 87 +++++++++++++++ .../Pages/Components/Form/SwitchDoc.razor | 81 ++++++++++++++ .../Pages/Components/Form/TextareaDoc.razor | 63 +++++++++++ 7 files changed, 560 insertions(+) create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/Form/ButtonDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/Form/CheckboxDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/Form/InputDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/Form/LabelDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SelectDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SwitchDoc.razor create mode 100644 NET9/BlazorInteractiveServer/Components/Pages/Components/Form/TextareaDoc.razor diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/ButtonDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/ButtonDoc.razor new file mode 100644 index 0000000..a3c8d24 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/ButtonDoc.razor @@ -0,0 +1,101 @@ +@page "/components/form/button" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components +@using BlazorInteractiveServer.Components.UI.Variants + +Button | ShellUI + +
    +
    +

    Button

    +

    Interactive button component with type-safe variants, sizes, and loading state.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add button + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    + +
    + +

    Variants

    +
    + + + + + + +
    + +

    Sizes

    +
    + + + + +
    + +

    Loading and Disabled

    +
    + + +
    + +

    With Icon

    +
    + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    VariantButtonVariantDefaultVisual style
    SizeButtonSizeDefaultButton size
    Typestring"button"HTML button type
    DisabledboolfalseDisables the button
    IsLoadingboolfalseShows loading spinner
    OnClickEventCallbackClick event callback
    +
    +
    + +
    +

    Accessibility

    +
      +
    • Screen reader compatible
    • +
    • Full keyboard navigation with visible focus indicators
    • +
    • aria-disabled applied when disabled or loading
    • +
    +
    +
    + +@code { + private bool isLoading = false; +} diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/CheckboxDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/CheckboxDoc.razor new file mode 100644 index 0000000..3d2eb4a --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/CheckboxDoc.razor @@ -0,0 +1,89 @@ +@page "/components/form/checkbox" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Checkbox | ShellUI + +
    +
    +

    Checkbox

    +

    Boolean input with checkbox styling and two-way binding.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add checkbox + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    + +

    Checked: @isChecked

    +
    + +

    Two-way Binding

    +
    + +

    Checked: @isChecked2

    +
    + +

    With Label

    +
    + + +
    + +

    Disabled

    +
    + + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    CheckedboolfalseChecked state
    CheckedChangedEventCallback<bool>Fires when checked state changes
    DisabledboolfalseDisables the checkbox
    Classstring?nullAdditional CSS classes
    +
    +
    + +
    +

    Accessibility

    +
      +
    • Proper ARIA attributes (role="checkbox", aria-checked)
    • +
    • Keyboard navigation support (Space to toggle)
    • +
    • Screen reader compatible
    • +
    +
    +
    + +@code { + private bool isChecked = false; + private bool isChecked2 = false; + private bool acceptTerms = false; + + private void HandleChanged(bool value) => isChecked = value; +} diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/InputDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/InputDoc.razor new file mode 100644 index 0000000..f2ca12c --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/InputDoc.razor @@ -0,0 +1,78 @@ +@page "/components/form/input" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Input | ShellUI + +
    +
    +

    Input

    +

    Text input component with variants and two-way binding.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add input + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    + + +
    + +

    Variants

    +
    +
    + + +
    +
    + + +
    +
    + +

    Disabled

    +
    + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    Valuestring""Input value
    VariantInputVariantDefaultVisual style
    Typestring"text"HTML input type
    Placeholderstring""Placeholder text
    DisabledboolfalseDisables the input
    +
    +
    +
    + +@code { + private string email = ""; + private string value1 = ""; + private string value2 = ""; +} diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/LabelDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/LabelDoc.razor new file mode 100644 index 0000000..2fb1b90 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/LabelDoc.razor @@ -0,0 +1,61 @@ +@page "/components/form/label" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI + +Label | ShellUI + +
    +
    +

    Label

    +

    Accessible label for form controls.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add label + +
    +
    + +
    +

    Usage

    + +

    With Input

    +
    + + +
    + +

    With Checkbox

    +
    + + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + +
    ParameterTypeDescription
    Forstring?ID of associated form control
    ChildContentRenderFragmentLabel text
    +
    +
    +
    + +@code { + private string email = ""; + private bool acceptTerms = false; +} diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SelectDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SelectDoc.razor new file mode 100644 index 0000000..e391e32 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SelectDoc.razor @@ -0,0 +1,87 @@ +@page "/components/form/select" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI + +Select | ShellUI + +
    +
    +

    Select

    +

    Dropdown select component for single selection.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add select + +
    +
    + +
    +

    Usage

    + +

    Basic

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

    Selected: @selectedFruit

    + } +
    + +

    Two-way Binding

    +
    + + +
    + +

    Disabled

    +
    + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    Valuestring?nullSelected value
    ValueChangedEventCallback<string>Fires when selection changes
    DisabledboolfalseDisables the select
    ChildContentRenderFragmentOption elements
    +
    +
    +
    + +@code { + private string selectedFruit = ""; + private string selectedFramework = ""; + + private void HandleSelectedFruitChanged(string value) => selectedFruit = value; +} diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SwitchDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SwitchDoc.razor new file mode 100644 index 0000000..fbdfee1 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/SwitchDoc.razor @@ -0,0 +1,81 @@ +@page "/components/form/switch" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI + +Switch | ShellUI + +
    +
    +

    Switch

    +

    Toggle switch for boolean on/off states.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add switch + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    + + @(isOn ? "On" : "Off") +
    + +

    With Label

    +
    + + +
    + +

    Disabled

    +
    + + +
    +
    + +
    +

    API Reference

    +
    + + + + + + + + + + + + + + + +
    ParameterTypeDefaultDescription
    CheckedboolfalseChecked state
    CheckedChangedEventCallback<bool>Fires when checked state changes
    DisabledboolfalseDisables the switch
    Classstring?nullAdditional CSS classes
    +
    +
    + +
    +

    Accessibility

    +
      +
    • Proper ARIA attributes (role="switch", aria-checked)
    • +
    • Keyboard navigation support
    • +
    • Screen reader compatible
    • +
    +
    +
    + +@code { + private bool isOn = false; + private bool notifications = true; + + private void HandleChanged(bool value) => isOn = value; +} diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/TextareaDoc.razor b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/TextareaDoc.razor new file mode 100644 index 0000000..198f6c6 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/Pages/Components/Form/TextareaDoc.razor @@ -0,0 +1,63 @@ +@page "/components/form/textarea" +@layout BlazorInteractiveServer.Components.Layout.DashboardLayout +@rendermode InteractiveServer +@using BlazorInteractiveServer.Components.UI +@using BlazorInteractiveServer.Components + +Textarea | ShellUI + +
    +
    +

    Textarea

    +

    Multi-line text input for longer content.

    +
    + +
    +

    Installation

    +
    + dotnet shellui add textarea + +
    +
    + +
    +

    Usage

    + +

    Basic

    +
    + +