From 230064564add127ddf0fb71663e1edfca3635bd8 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 12 Oct 2025 03:14:37 +0200 Subject: [PATCH 01/45] feat: Configure CLI tool with command structure - Updated ShellUI.CLI.csproj as proper dotnet tool - Added PackAsTool configuration with metadata - Implemented Program.cs with all commands: * init - Initialize ShellUI in project * add - Add components (supports space/comma-separated) * list - List available components * remove - Remove components * update - Update components - Beautiful CLI output with Spectre.Console - FigletText banner for ShellUI - Tables for component display - All commands currently show 'Coming soon' messages --- src/ShellUI.CLI/Program.cs | 210 ++++++++++++++++++++++++++++- src/ShellUI.CLI/ShellUI.CLI.csproj | 17 +++ 2 files changed, 225 insertions(+), 2 deletions(-) diff --git a/src/ShellUI.CLI/Program.cs b/src/ShellUI.CLI/Program.cs index 3751555..1e6b26a 100644 --- a/src/ShellUI.CLI/Program.cs +++ b/src/ShellUI.CLI/Program.cs @@ -1,2 +1,208 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using System.CommandLine; +using Spectre.Console; + +namespace ShellUI.CLI; + +class Program +{ + static async Task Main(string[] args) + { + var rootCommand = new RootCommand("ShellUI - CLI-first Blazor component library") + { + Description = "Add beautiful, accessible components to your Blazor app. Inspired by shadcn/ui." + }; + + // Add commands + rootCommand.AddCommand(CreateInitCommand()); + rootCommand.AddCommand(CreateAddCommand()); + rootCommand.AddCommand(CreateListCommand()); + rootCommand.AddCommand(CreateRemoveCommand()); + rootCommand.AddCommand(CreateUpdateCommand()); + + return await rootCommand.InvokeAsync(args); + } + + static Command CreateInitCommand() + { + var command = new Command("init", "Initialize ShellUI in your Blazor project"); + + var forceOption = new Option( + "--force", + "Reinitialize even if already initialized"); + var styleOption = new Option( + "--style", + getDefaultValue: () => "default", + "Choose component style (default, new-york, minimal)"); + + command.AddOption(forceOption); + command.AddOption(styleOption); + + command.SetHandler((force, style) => + { + AnsiConsole.Write(new FigletText("ShellUI").Color(Color.Blue)); + AnsiConsole.MarkupLine("[green]Initializing ShellUI...[/]"); + AnsiConsole.MarkupLine($"[dim]Style: {style}[/]"); + + if (force) + { + AnsiConsole.MarkupLine("[yellow]Force mode enabled[/]"); + } + + AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon in Milestone 1![/]"); + AnsiConsole.MarkupLine("[dim]This will:[/]"); + AnsiConsole.MarkupLine("[dim] • Detect your project type (Server/WASM/SSR)[/]"); + AnsiConsole.MarkupLine("[dim] • Create Components/UI folder[/]"); + AnsiConsole.MarkupLine("[dim] • Download Tailwind standalone CLI (no Node.js!)[/]"); + AnsiConsole.MarkupLine("[dim] • Create shellui.json config[/]"); + AnsiConsole.MarkupLine("[dim] • Set up Tailwind CSS v4[/]"); + }, forceOption, styleOption); + + return command; + } + + static Command CreateAddCommand() + { + var command = new Command("add", "Add component(s) to your project"); + + var componentsArg = new Argument( + "components", + "Component name(s) to add (space or comma-separated)") + { + Arity = ArgumentArity.OneOrMore + }; + command.AddArgument(componentsArg); + + var forceOption = new Option( + "--force", + "Overwrite existing components"); + command.AddOption(forceOption); + + command.SetHandler((components, force) => + { + AnsiConsole.MarkupLine("[green]Adding components:[/]"); + + var table = new Table(); + table.AddColumn("Component"); + table.AddColumn("Status"); + + foreach (var component in components) + { + table.AddRow(component, "[yellow]Coming soon[/]"); + } + + AnsiConsole.Write(table); + + if (force) + { + AnsiConsole.MarkupLine("\n[yellow]Force mode: Will overwrite existing components[/]"); + } + + AnsiConsole.MarkupLine("\n[dim]Usage will be:[/]"); + AnsiConsole.MarkupLine("[dim] dotnet shellui add button card alert[/]"); + AnsiConsole.MarkupLine("[dim] dotnet shellui add button,card,alert[/]"); + }, componentsArg, forceOption); + + return command; + } + + static Command CreateListCommand() + { + var command = new Command("list", "List available components"); + + var installedOption = new Option( + "--installed", + "Show only installed components"); + var availableOption = new Option( + "--available", + "Show only available components"); + + command.AddOption(installedOption); + command.AddOption(availableOption); + + command.SetHandler((installed, available) => + { + AnsiConsole.MarkupLine("[blue]ShellUI Components[/]\n"); + + var table = new Table(); + table.AddColumn("Component"); + table.AddColumn("Status"); + table.AddColumn("Category"); + table.AddColumn("Description"); + + // Mock data for demonstration + table.AddRow("button", "[green]Available[/]", "Form", "Interactive button"); + table.AddRow("card", "[green]Available[/]", "Layout", "Content container"); + table.AddRow("alert", "[green]Available[/]", "Feedback", "Alert messages"); + table.AddRow("input", "[green]Available[/]", "Form", "Text input field"); + table.AddRow("dialog", "[green]Available[/]", "Overlay", "Modal dialogs"); + + AnsiConsole.Write(table); + + AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon: 40+ components![/]"); + }, installedOption, availableOption); + + return command; + } + + static Command CreateRemoveCommand() + { + var command = new Command("remove", "Remove component(s) from your project"); + + var componentsArg = new Argument( + "components", + "Component name(s) to remove") + { + Arity = ArgumentArity.OneOrMore + }; + command.AddArgument(componentsArg); + + command.SetHandler((components) => + { + AnsiConsole.MarkupLine("[red]Removing components:[/]"); + foreach (var component in components) + { + AnsiConsole.MarkupLine($" • {component}"); + } + AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon in Milestone 1![/]"); + }, componentsArg); + + return command; + } + + static Command CreateUpdateCommand() + { + var command = new Command("update", "Update component(s) to latest version"); + + var componentsArg = new Argument( + "components", + "Component name(s) to update (empty = all)") + { + Arity = ArgumentArity.ZeroOrMore + }; + command.AddArgument(componentsArg); + + var allOption = new Option( + "--all", + "Update all installed components"); + command.AddOption(allOption); + + command.SetHandler((components, all) => + { + if (all || components.Length == 0) + { + AnsiConsole.MarkupLine("[blue]Updating all components...[/]"); + } + else + { + AnsiConsole.MarkupLine("[blue]Updating specific components:[/]"); + foreach (var component in components) + { + AnsiConsole.MarkupLine($" • {component}"); + } + } + AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon in Milestone 1![/]"); + }, componentsArg, allOption); + + return command; + } +} diff --git a/src/ShellUI.CLI/ShellUI.CLI.csproj b/src/ShellUI.CLI/ShellUI.CLI.csproj index 96b518a..a8fae8d 100644 --- a/src/ShellUI.CLI/ShellUI.CLI.csproj +++ b/src/ShellUI.CLI/ShellUI.CLI.csproj @@ -15,6 +15,23 @@ net9.0 enable enable + + + true + shellui + ShellUI.CLI + 0.1.0-alpha + Shell Technologies + CLI tool for ShellUI - Blazor component library inspired by shadcn/ui + blazor;components;cli;shadcn;tailwind;dotnet + https://github.com/Shell-Technologies/shellui + https://github.com/Shell-Technologies/shellui + MIT + README.md + + + + From 3ada934b1cb39830da7fde092be3fe62a7e897e4 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sun, 12 Oct 2025 03:55:04 +0200 Subject: [PATCH 02/45] refactor: Replace Sysinfocus components with ShellUI components - Updated project references in BlazorInteractiveServer.csproj to use ShellUI.Components. - Removed Sysinfocus component references from Program.cs and _Imports.razor. - Enhanced App.razor with dark mode support and Tailwind CSS integration. - Redesigned MainLayout.razor for improved layout and dark mode toggle functionality. - Revamped Home.razor to showcase ShellUI button component with various states and sizes. - Introduced new Button component in ShellUI.Components with customizable properties. - Updated app.css for Tailwind CSS theming and styling adjustments. --- .../BlazorInteractiveServer.csproj | 2 +- .../Components/App.razor | 42 ++++- .../Components/Layout/MainLayout.razor | 55 ++++-- .../Components/Pages/Home.razor | 90 ++++++++-- .../Components/_Imports.razor | 2 +- NET9/BlazorInteractiveServer/Program.cs | 3 - NET9/BlazorInteractiveServer/wwwroot/app.css | 156 +++++++++++++++--- src/ShellUI.CLI/Program.cs | 82 ++++++--- .../Components/Button.razor | 106 ++++++++++++ src/ShellUI.Core/Class1.cs | 6 - src/ShellUI.Core/Models/ComponentCategory.cs | 15 ++ src/ShellUI.Core/Models/ComponentMetadata.cs | 19 +++ src/ShellUI.Core/Models/ShellUIConfig.cs | 37 +++++ src/ShellUI.Templates/Class1.cs | 6 - src/ShellUI.Templates/ComponentRegistry.cs | 42 +++++ .../ShellUI.Templates.csproj | 4 + .../Templates/ButtonTemplate.cs | 126 ++++++++++++++ 17 files changed, 695 insertions(+), 98 deletions(-) create mode 100644 src/ShellUI.Components/Components/Button.razor delete mode 100644 src/ShellUI.Core/Class1.cs create mode 100644 src/ShellUI.Core/Models/ComponentCategory.cs create mode 100644 src/ShellUI.Core/Models/ComponentMetadata.cs create mode 100644 src/ShellUI.Core/Models/ShellUIConfig.cs delete mode 100644 src/ShellUI.Templates/Class1.cs create mode 100644 src/ShellUI.Templates/ComponentRegistry.cs create mode 100644 src/ShellUI.Templates/Templates/ButtonTemplate.cs diff --git a/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj index 79b14f8..e61a160 100644 --- a/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj +++ b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj @@ -7,7 +7,7 @@ - + diff --git a/NET9/BlazorInteractiveServer/Components/App.razor b/NET9/BlazorInteractiveServer/Components/App.razor index f21df6d..9b63d78 100644 --- a/NET9/BlazorInteractiveServer/Components/App.razor +++ b/NET9/BlazorInteractiveServer/Components/App.razor @@ -1,19 +1,53 @@  - + + + + + + - - + + + + - + diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index 13ec0ff..3c9d7ef 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -1,26 +1,45 @@ @inherits LayoutComponentBase -@inject Initialization init +@inject IJSRuntime JSRuntime -
-
-

Brand Name

+
+
+
+
+

ShellUI

+ +
+
+
- -
- @Body +
+ @Body +
+ +
+
+ Built with ShellUI - CLI-first Blazor component library +
+
An unhandled error has occurred. Reload - 🗙 -
- -@code -{ - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) await init.InitializeTheme(); - } -} \ No newline at end of file + x +
\ No newline at end of file diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor index 00fbada..a55e432 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor @@ -1,19 +1,85 @@ @page "/" +@using ShellUI.Components -Home +ShellUI - Button Demo -
-

Hello, world!

- + + + + + +
+ + + +
+

Sizes

+
+ + + + +
+
+ + +
+

States

+
+ + + +
+
+ + + @if (clickCount > 0) + { +
+

+ Button clicked @clickCount time(s)! +

+
+ } + + +
+

Installation

+ dotnet shellui add button +

+ Component will be copied to your Components/UI folder for full customization! +

+
+ + +@code { + private int clickCount = 0; + private bool isLoading = false; + + private async Task HandleClick(MouseEventArgs args) { - - - + clickCount++; } - -@code -{ - bool showAlert; + private async Task HandleLoadingClick(MouseEventArgs args) + { + isLoading = true; + clickCount++; + await Task.Delay(2000); // Simulate async operation + isLoading = false; + } } \ No newline at end of file diff --git a/NET9/BlazorInteractiveServer/Components/_Imports.razor b/NET9/BlazorInteractiveServer/Components/_Imports.razor index 2f61f4b..adb94bc 100644 --- a/NET9/BlazorInteractiveServer/Components/_Imports.razor +++ b/NET9/BlazorInteractiveServer/Components/_Imports.razor @@ -9,4 +9,4 @@ @using BlazorInteractiveServer @using BlazorInteractiveServer.Components -@using Sysinfocus.AspNetCore.Components +@using ShellUI.Components diff --git a/NET9/BlazorInteractiveServer/Program.cs b/NET9/BlazorInteractiveServer/Program.cs index 0b4bab5..d08e0ee 100644 --- a/NET9/BlazorInteractiveServer/Program.cs +++ b/NET9/BlazorInteractiveServer/Program.cs @@ -1,5 +1,4 @@ using BlazorInteractiveServer.Components; -using Sysinfocus.AspNetCore.Components; var builder = WebApplication.CreateBuilder(args); @@ -7,8 +6,6 @@ builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); -builder.Services.AddSysinfocus(); - var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index 0ca86a1..f7f140a 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -1,46 +1,152 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box +/* ShellUI - Tailwind CSS v4 Setup */ + +/* We're using Tailwind Play CDN from the HTML head, but we define our theme here */ + +:root { + /* Light mode colors */ + --color-background: 255 255 255; + --color-foreground: 15 23 42; + --color-card: 255 255 255; + --color-card-foreground: 15 23 42; + --color-popover: 255 255 255; + --color-popover-foreground: 15 23 42; + --color-primary: 59 130 246; + --color-primary-foreground: 255 255 255; + --color-secondary: 100 116 139; + --color-secondary-foreground: 255 255 255; + --color-muted: 241 245 249; + --color-muted-foreground: 100 116 139; + --color-accent: 241 245 249; + --color-accent-foreground: 15 23 42; + --color-destructive: 239 68 68; + --color-destructive-foreground: 255 255 255; + --color-border: 226 232 240; + --color-input: 226 232 240; + --color-ring: 59 130 246; + --radius: 0.5rem; +} + +/* Dark mode colors */ +.dark { + --color-background: 15 23 42; + --color-foreground: 248 250 252; + --color-card: 15 23 42; + --color-card-foreground: 248 250 252; + --color-popover: 15 23 42; + --color-popover-foreground: 248 250 252; + --color-primary: 59 130 246; + --color-primary-foreground: 255 255 255; + --color-secondary: 71 85 105; + --color-secondary-foreground: 248 250 252; + --color-muted: 30 41 59; + --color-muted-foreground: 148 163 184; + --color-accent: 30 41 59; + --color-accent-foreground: 248 250 252; + --color-destructive: 220 38 38; + --color-destructive-foreground: 255 255 255; + --color-border: 30 41 59; + --color-input: 30 41 59; + --color-ring: 59 130 246; +} + +/* Apply colors using rgb() */ +.bg-background { + background-color: rgb(var(--color-background)); +} + +.text-foreground { + color: rgb(var(--color-foreground)); +} + +.bg-primary { + background-color: rgb(var(--color-primary)); +} + +.text-primary-foreground { + color: rgb(var(--color-primary-foreground)); +} + +.bg-secondary { + background-color: rgb(var(--color-secondary)); +} + +.text-secondary-foreground { + color: rgb(var(--color-secondary-foreground)); +} + +.bg-destructive { + background-color: rgb(var(--color-destructive)); +} + +.text-destructive-foreground { + color: rgb(var(--color-destructive-foreground)); +} + +.bg-muted { + background-color: rgb(var(--color-muted)); } -.container { - max-width: 1400px; - margin: auto; - padding: 1rem +.text-muted-foreground { + color: rgb(var(--color-muted-foreground)); } +.bg-accent { + background-color: rgb(var(--color-accent)); +} + +.text-accent-foreground { + color: rgb(var(--color-accent-foreground)); +} + +.border-input { + border-color: rgb(var(--color-border)); +} + +.ring-ring { + --tw-ring-color: rgb(var(--color-ring)); +} + +/* Hover variants */ +.hover\:bg-primary\/90:hover { + background-color: rgb(var(--color-primary) / 0.9); +} + +.hover\:bg-secondary\/80:hover { + background-color: rgb(var(--color-secondary) / 0.8); +} + +.hover\:bg-destructive\/90:hover { + background-color: rgb(var(--color-destructive) / 0.9); +} + +.hover\:bg-accent:hover { + background-color: rgb(var(--color-accent)); +} + +.hover\:text-accent-foreground:hover { + color: rgb(var(--color-accent-foreground)); +} + +/* Blazor validation styles */ .valid.modified:not([type=checkbox]) { outline: 1px solid #26b050; } .invalid { - outline: 1px solid #e50000; + outline: 1px solid rgb(var(--color-destructive)); } .validation-message { - color: #e50000; + color: rgb(var(--color-destructive)); } +/* Blazor error boundary */ .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; padding: 1rem 1rem 1rem 3.7rem; color: white; } - .blazor-error-boundary::after { - content: "An error has occurred." - } - -.darker-border-checkbox.form-check-input { - border-color: #929292; +.blazor-error-boundary::after { + content: "An error has occurred." } - -.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { - color: var(--bs-secondary-color); - text-align: end; -} - -.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { - text-align: start; -} \ No newline at end of file diff --git a/src/ShellUI.CLI/Program.cs b/src/ShellUI.CLI/Program.cs index 1e6b26a..6f46103 100644 --- a/src/ShellUI.CLI/Program.cs +++ b/src/ShellUI.CLI/Program.cs @@ -1,5 +1,7 @@ using System.CommandLine; using Spectre.Console; +using ShellUI.Templates; +using ShellUI.Core.Models; namespace ShellUI.CLI; @@ -48,13 +50,13 @@ static Command CreateInitCommand() AnsiConsole.MarkupLine("[yellow]Force mode enabled[/]"); } - AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon in Milestone 1![/]"); + AnsiConsole.MarkupLine("\n[yellow]Coming soon in Milestone 1![/]"); AnsiConsole.MarkupLine("[dim]This will:[/]"); - AnsiConsole.MarkupLine("[dim] • Detect your project type (Server/WASM/SSR)[/]"); - AnsiConsole.MarkupLine("[dim] • Create Components/UI folder[/]"); - AnsiConsole.MarkupLine("[dim] • Download Tailwind standalone CLI (no Node.js!)[/]"); - AnsiConsole.MarkupLine("[dim] • Create shellui.json config[/]"); - AnsiConsole.MarkupLine("[dim] • Set up Tailwind CSS v4[/]"); + AnsiConsole.MarkupLine("[dim] - Detect your project type (Server/WASM/SSR)[/]"); + AnsiConsole.MarkupLine("[dim] - Create Components/UI folder[/]"); + AnsiConsole.MarkupLine("[dim] - Download Tailwind standalone CLI (no Node.js!)[/]"); + AnsiConsole.MarkupLine("[dim] - Create shellui.json config[/]"); + AnsiConsole.MarkupLine("[dim] - Set up Tailwind CSS v4[/]"); }, forceOption, styleOption); return command; @@ -79,15 +81,40 @@ static Command CreateAddCommand() command.SetHandler((components, force) => { - AnsiConsole.MarkupLine("[green]Adding components:[/]"); + AnsiConsole.MarkupLine("[green]Adding components:[/]\n"); + + // Parse comma-separated components + var componentList = new List(); + foreach (var comp in components) + { + componentList.AddRange(comp.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } var table = new Table(); table.AddColumn("Component"); table.AddColumn("Status"); + table.AddColumn("Version"); - foreach (var component in components) + foreach (var componentName in componentList) { - table.AddRow(component, "[yellow]Coming soon[/]"); + var exists = ComponentRegistry.Exists(componentName); + if (exists) + { + var metadata = ComponentRegistry.GetMetadata(componentName); + table.AddRow( + componentName, + "[yellow]Implementation pending[/]", + $"[dim]{metadata?.Version ?? "0.1.0"}[/]" + ); + } + else + { + table.AddRow( + componentName, + "[red]Not found[/]", + "[dim]-[/]" + ); + } } AnsiConsole.Write(table); @@ -97,9 +124,11 @@ static Command CreateAddCommand() AnsiConsole.MarkupLine("\n[yellow]Force mode: Will overwrite existing components[/]"); } - AnsiConsole.MarkupLine("\n[dim]Usage will be:[/]"); + AnsiConsole.MarkupLine("\n[dim]Multiple component syntax supported:[/]"); AnsiConsole.MarkupLine("[dim] dotnet shellui add button card alert[/]"); AnsiConsole.MarkupLine("[dim] dotnet shellui add button,card,alert[/]"); + + AnsiConsole.MarkupLine("\n[yellow]Component copying implementation coming soon![/]"); }, componentsArg, forceOption); return command; @@ -125,20 +154,29 @@ static Command CreateListCommand() var table = new Table(); table.AddColumn("Component"); - table.AddColumn("Status"); + table.AddColumn("Version"); table.AddColumn("Category"); table.AddColumn("Description"); - // Mock data for demonstration - table.AddRow("button", "[green]Available[/]", "Form", "Interactive button"); - table.AddRow("card", "[green]Available[/]", "Layout", "Content container"); - table.AddRow("alert", "[green]Available[/]", "Feedback", "Alert messages"); - table.AddRow("input", "[green]Available[/]", "Form", "Text input field"); - table.AddRow("dialog", "[green]Available[/]", "Overlay", "Modal dialogs"); + // Get components from registry + foreach (var component in ComponentRegistry.Components.Values) + { + if (component.IsAvailable) + { + table.AddRow( + component.Name, + $"[dim]{component.Version}[/]", + component.Category.ToString(), + component.Description + ); + } + } AnsiConsole.Write(table); - AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon: 40+ components![/]"); + var totalCount = ComponentRegistry.Components.Count; + AnsiConsole.MarkupLine($"\n[green]{totalCount} component(s) available[/]"); + AnsiConsole.MarkupLine("[yellow]More coming soon: Target 40+ components![/]"); }, installedOption, availableOption); return command; @@ -161,9 +199,9 @@ static Command CreateRemoveCommand() AnsiConsole.MarkupLine("[red]Removing components:[/]"); foreach (var component in components) { - AnsiConsole.MarkupLine($" • {component}"); + AnsiConsole.MarkupLine($" - {component}"); } - AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon in Milestone 1![/]"); + AnsiConsole.MarkupLine("\n[yellow]Coming soon in Milestone 1![/]"); }, componentsArg); return command; @@ -197,10 +235,10 @@ static Command CreateUpdateCommand() AnsiConsole.MarkupLine("[blue]Updating specific components:[/]"); foreach (var component in components) { - AnsiConsole.MarkupLine($" • {component}"); + AnsiConsole.MarkupLine($" - {component}"); } } - AnsiConsole.MarkupLine("\n[yellow]⏳ Coming soon in Milestone 1![/]"); + AnsiConsole.MarkupLine("\n[yellow]Coming soon in Milestone 1![/]"); }, componentsArg, allOption); return command; diff --git a/src/ShellUI.Components/Components/Button.razor b/src/ShellUI.Components/Components/Button.razor new file mode 100644 index 0000000..5b20360 --- /dev/null +++ b/src/ShellUI.Components/Components/Button.razor @@ -0,0 +1,106 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string Size { get; set; } = "md"; + + [Parameter] + public string Type { get; set; } = "button"; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public bool IsLoading { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + // Base styles + "inline-flex", + "items-center", + "justify-center", + "gap-2", + "whitespace-nowrap", + "rounded-md", + "text-sm", + "font-medium", + "ring-offset-background", + "transition-colors", + + // Focus styles + "focus-visible:outline-none", + "focus-visible:ring-2", + "focus-visible:ring-ring", + "focus-visible:ring-offset-2", + + // Disabled styles + "disabled:pointer-events-none", + "disabled:opacity-50" + }; + + // Variant styles + classes.Add(Variant.ToLower() switch + { + "default" => "bg-primary text-primary-foreground hover:bg-primary/90", + "destructive" => "bg-destructive text-destructive-foreground hover:bg-destructive/90", + "outline" => "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + "secondary" => "bg-secondary text-secondary-foreground hover:bg-secondary/80", + "ghost" => "hover:bg-accent hover:text-accent-foreground", + "link" => "text-primary underline-offset-4 hover:underline", + _ => "bg-primary text-primary-foreground hover:bg-primary/90" + }); + + // Size styles + classes.Add(Size.ToLower() switch + { + "sm" => "h-9 rounded-md px-3", + "md" => "h-10 px-4 py-2", + "lg" => "h-11 rounded-md px-8", + "icon" => "h-10 w-10", + _ => "h-10 px-4 py-2" + }); + + return string.Join(" ", classes); + } + + private async Task HandleClick(MouseEventArgs args) + { + if (!Disabled && !IsLoading) + { + await OnClick.InvokeAsync(args); + } + } +} + diff --git a/src/ShellUI.Core/Class1.cs b/src/ShellUI.Core/Class1.cs deleted file mode 100644 index 824ce8f..0000000 --- a/src/ShellUI.Core/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ShellUI.Core; - -public class Class1 -{ - -} diff --git a/src/ShellUI.Core/Models/ComponentCategory.cs b/src/ShellUI.Core/Models/ComponentCategory.cs new file mode 100644 index 0000000..1e58f18 --- /dev/null +++ b/src/ShellUI.Core/Models/ComponentCategory.cs @@ -0,0 +1,15 @@ +namespace ShellUI.Core.Models; + +public enum ComponentCategory +{ + Form, + Layout, + Feedback, + Overlay, + Navigation, + DataDisplay, + Typography, + Media, + Utility +} + diff --git a/src/ShellUI.Core/Models/ComponentMetadata.cs b/src/ShellUI.Core/Models/ComponentMetadata.cs new file mode 100644 index 0000000..b7826eb --- /dev/null +++ b/src/ShellUI.Core/Models/ComponentMetadata.cs @@ -0,0 +1,19 @@ +namespace ShellUI.Core.Models; + +public class ComponentMetadata +{ + public required string Name { get; set; } + public required string DisplayName { get; set; } + public required string Description { get; set; } + public required ComponentCategory Category { get; set; } + public List Dependencies { get; set; } = new(); + + // Relative to Components/UI folder + public required string FilePath { get; set; } + + public bool IsAvailable { get; set; } = true; + public string Version { get; set; } = "0.1.0"; + public List Variants { get; set; } = new(); + public List Tags { get; set; } = new(); +} + diff --git a/src/ShellUI.Core/Models/ShellUIConfig.cs b/src/ShellUI.Core/Models/ShellUIConfig.cs new file mode 100644 index 0000000..c3e961f --- /dev/null +++ b/src/ShellUI.Core/Models/ShellUIConfig.cs @@ -0,0 +1,37 @@ +namespace ShellUI.Core.Models; + +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 TailwindConfig Tailwind { get; set; } = new(); + public List InstalledComponents { get; set; } = new(); + public ProjectType ProjectType { get; set; } +} + +public class TailwindConfig +{ + public bool Enabled { get; set; } = true; + public string Version { get; set; } = "4.0.0"; + public string ConfigPath { get; set; } = "tailwind.config.js"; + public string CssPath { get; set; } = "Styles/app.css"; +} + +public class InstalledComponent +{ + public required string Name { get; set; } + public required string Version { get; set; } + public DateTime InstalledAt { get; set; } = DateTime.UtcNow; + public bool IsCustomized { get; set; } +} + +public enum ProjectType +{ + Unknown, + BlazorServer, + BlazorWebAssembly, + BlazorServerSideRendering, + BlazorUnited +} + diff --git a/src/ShellUI.Templates/Class1.cs b/src/ShellUI.Templates/Class1.cs deleted file mode 100644 index a171774..0000000 --- a/src/ShellUI.Templates/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ShellUI.Templates; - -public class Class1 -{ - -} diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs new file mode 100644 index 0000000..4f7bacf --- /dev/null +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -0,0 +1,42 @@ +using ShellUI.Core.Models; +using ShellUI.Templates.Templates; + +namespace ShellUI.Templates; + +public static class ComponentRegistry +{ + public static readonly Dictionary Components = new() + { + { "button", ButtonTemplate.Metadata } + }; + + public static string? GetComponentContent(string componentName) + { + return componentName.ToLower() switch + { + "button" => ButtonTemplate.Content, + _ => null + }; + } + + public static IEnumerable GetByCategory(ComponentCategory category) + { + return Components.Values.Where(c => c.Category == category); + } + + public static IEnumerable SearchByTag(string tag) + { + return Components.Values.Where(c => c.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase)); + } + + public static ComponentMetadata? GetMetadata(string componentName) + { + return Components.TryGetValue(componentName.ToLower(), out var metadata) ? metadata : null; + } + + public static bool Exists(string componentName) + { + return Components.ContainsKey(componentName.ToLower()); + } +} + diff --git a/src/ShellUI.Templates/ShellUI.Templates.csproj b/src/ShellUI.Templates/ShellUI.Templates.csproj index 125f4c9..c9ad116 100644 --- a/src/ShellUI.Templates/ShellUI.Templates.csproj +++ b/src/ShellUI.Templates/ShellUI.Templates.csproj @@ -1,5 +1,9 @@  + + + + net9.0 enable diff --git a/src/ShellUI.Templates/Templates/ButtonTemplate.cs b/src/ShellUI.Templates/Templates/ButtonTemplate.cs new file mode 100644 index 0000000..290a413 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ButtonTemplate.cs @@ -0,0 +1,126 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class ButtonTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "button", + DisplayName = "Button", + Description = "Interactive button component with multiple variants and sizes", + Category = ComponentCategory.Form, + FilePath = "Button.razor", + Version = "0.1.0", + Variants = new List { "default", "destructive", "outline", "secondary", "ghost", "link" }, + Tags = new List { "form", "input", "interactive", "button", "action" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public string Variant { get; set; } = ""default""; + + [Parameter] + public string Size { get; set; } = ""md""; + + [Parameter] + public string Type { get; set; } = ""button""; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public bool IsLoading { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + // Base styles + ""inline-flex"", + ""items-center"", + ""justify-center"", + ""gap-2"", + ""whitespace-nowrap"", + ""rounded-md"", + ""text-sm"", + ""font-medium"", + ""ring-offset-background"", + ""transition-colors"", + + // Focus styles + ""focus-visible:outline-none"", + ""focus-visible:ring-2"", + ""focus-visible:ring-ring"", + ""focus-visible:ring-offset-2"", + + // Disabled styles + ""disabled:pointer-events-none"", + ""disabled:opacity-50"" + }; + + // Variant styles + classes.Add(Variant.ToLower() switch + { + ""default"" => ""bg-primary text-primary-foreground hover:bg-primary/90"", + ""destructive"" => ""bg-destructive text-destructive-foreground hover:bg-destructive/90"", + ""outline"" => ""border border-input bg-background hover:bg-accent hover:text-accent-foreground"", + ""secondary"" => ""bg-secondary text-secondary-foreground hover:bg-secondary/80"", + ""ghost"" => ""hover:bg-accent hover:text-accent-foreground"", + ""link"" => ""text-primary underline-offset-4 hover:underline"", + _ => ""bg-primary text-primary-foreground hover:bg-primary/90"" + }); + + // Size styles + classes.Add(Size.ToLower() switch + { + ""sm"" => ""h-9 rounded-md px-3"", + ""md"" => ""h-10 px-4 py-2"", + ""lg"" => ""h-11 rounded-md px-8"", + ""icon"" => ""h-10 w-10"", + _ => ""h-10 px-4 py-2"" + }); + + return string.Join("" "", classes); + } + + private async Task HandleClick(MouseEventArgs args) + { + if (!Disabled && !IsLoading) + { + await OnClick.InvokeAsync(args); + } + } +} +"; +} + From a09936cf1ed96c2639331c3c54a9c4c8e5ab97c0 Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 13 Oct 2025 22:55:53 +0200 Subject: [PATCH 03/45] chore: Remove unused Sysinfocus component reference from Home.razor --- NET9/BlazorInteractiveServer/Components/Pages/Home.razor | 1 - 1 file changed, 1 deletion(-) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor index a55e432..2b1ea3c 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor @@ -1,5 +1,4 @@ @page "/" -@using ShellUI.Components ShellUI - Button Demo From 3d6eb1dfdfa0867fb5b60cdcc416503f122ad9e6 Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 13 Oct 2025 22:57:17 +0200 Subject: [PATCH 04/45] feat: Implement ComponentInstaller for managing ShellUI components --- .../Services/ComponentInstaller.cs | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/ShellUI.CLI/Services/ComponentInstaller.cs diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs new file mode 100644 index 0000000..417392a --- /dev/null +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -0,0 +1,149 @@ +using ShellUI.Core.Models; +using ShellUI.Templates; +using System.Text.Json; +using Spectre.Console; + +namespace ShellUI.CLI.Services; + +public class ComponentInstaller +{ + public static void InstallComponents(string[] components, bool force) + { + var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); + + if (!File.Exists(configPath)) + { + AnsiConsole.MarkupLine("[red]ShellUI not initialized![/]"); + AnsiConsole.MarkupLine("[yellow]Run 'dotnet shellui init' first[/]"); + return; + } + + // Load config + var configJson = File.ReadAllText(configPath); + var config = JsonSerializer.Deserialize(configJson); + + if (config == null) + { + AnsiConsole.MarkupLine("[red]Failed to read shellui.json[/]"); + return; + } + + // Detect project for namespace + var projectInfo = ProjectDetector.DetectProject(); + + // Parse comma-separated components + var componentList = new List(); + foreach (var comp in components) + { + componentList.AddRange(comp.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + + var successCount = 0; + var skippedCount = 0; + var failedComponents = new List(); + + foreach (var componentName in componentList) + { + var result = InstallComponent(componentName, config, projectInfo, force); + + if (result == InstallResult.Success) + successCount++; + else if (result == InstallResult.Skipped) + skippedCount++; + else + failedComponents.Add(componentName); + } + + // Update config + var updatedJson = JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(configPath, updatedJson); + + // Summary + AnsiConsole.MarkupLine(""); + if (successCount > 0) + AnsiConsole.MarkupLine($"[green]Installed {successCount} component(s) successfully![/]"); + if (skippedCount > 0) + AnsiConsole.MarkupLine($"[yellow]Skipped {skippedCount} component(s) (already exists, use --force to overwrite)[/]"); + if (failedComponents.Count > 0) + AnsiConsole.MarkupLine($"[red]Failed: {string.Join(", ", failedComponents)}[/]"); + } + + private static InstallResult InstallComponent(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force) + { + if (!ComponentRegistry.Exists(componentName)) + { + AnsiConsole.MarkupLine($"[red]Component '{componentName}' not found[/]"); + return InstallResult.Failed; + } + + var metadata = ComponentRegistry.GetMetadata(componentName); + if (metadata == null) + { + AnsiConsole.MarkupLine($"[red]Failed to get metadata for '{componentName}'[/]"); + return InstallResult.Failed; + } + + var componentPath = Path.Combine(Directory.GetCurrentDirectory(), config.ComponentsPath, metadata.FilePath); + + // Check if already exists + if (File.Exists(componentPath) && !force) + { + AnsiConsole.MarkupLine($"[yellow]Skipped '{componentName}' (already exists)[/]"); + return InstallResult.Skipped; + } + + // Get component content + var content = ComponentRegistry.GetComponentContent(componentName); + if (content == null) + { + AnsiConsole.MarkupLine($"[red]Failed to get content for '{componentName}'[/]"); + return InstallResult.Failed; + } + + // Replace namespace placeholder + content = content.Replace("YourProjectNamespace", projectInfo.RootNamespace); + + // Ensure directory exists + var directory = Path.GetDirectoryName(componentPath); + if (directory != null) + { + Directory.CreateDirectory(directory); + } + + // Write file + File.WriteAllText(componentPath, content); + + // Update config + var existing = config.InstalledComponents.FirstOrDefault(c => c.Name == componentName); + if (existing != null) + { + existing.Version = metadata.Version; + existing.InstalledAt = DateTime.UtcNow; + existing.IsCustomized = false; + } + else + { + config.InstalledComponents.Add(new InstalledComponent + { + Name = componentName, + Version = metadata.Version, + InstalledAt = DateTime.UtcNow, + IsCustomized = false + }); + } + + AnsiConsole.MarkupLine($"[green]Installed '{componentName}'[/] [dim]({metadata.FilePath})[/]"); + return InstallResult.Success; + } + + private enum InstallResult + { + Success, + Skipped, + Failed + } +} + From 9e90324391cf2c928c18aae066dbb8c401d8e813 Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 13 Oct 2025 22:58:02 +0200 Subject: [PATCH 05/45] feat: Add InitService for ShellUI project initialization --- src/ShellUI.CLI/Services/InitService.cs | 80 +++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/ShellUI.CLI/Services/InitService.cs diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs new file mode 100644 index 0000000..f3feb0a --- /dev/null +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -0,0 +1,80 @@ +using ShellUI.Core.Models; +using System.Text.Json; +using Spectre.Console; + +namespace ShellUI.CLI.Services; + +public class InitService +{ + public static void Initialize(string style, bool force) + { + var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); + + if (File.Exists(configPath) && !force) + { + AnsiConsole.MarkupLine("[yellow]ShellUI is already initialized in this project.[/]"); + AnsiConsole.MarkupLine("[dim]Use --force to reinitialize[/]"); + return; + } + + AnsiConsole.Status() + .Start("Initializing ShellUI...", ctx => + { + // Step 1: Detect project + ctx.Status("Detecting project type..."); + var projectInfo = ProjectDetector.DetectProject(); + AnsiConsole.MarkupLine($"[green]Detected:[/] {projectInfo.ProjectType}"); + AnsiConsole.MarkupLine($"[dim]Project: {projectInfo.ProjectName}[/]"); + AnsiConsole.MarkupLine($"[dim]Namespace: {projectInfo.RootNamespace}[/]"); + + // Step 2: Create Components/UI folder + ctx.Status("Creating component folders..."); + var componentsPath = Path.Combine(Directory.GetCurrentDirectory(), "Components", "UI"); + Directory.CreateDirectory(componentsPath); + AnsiConsole.MarkupLine($"[green]Created:[/] Components/UI/"); + + // Step 3: Create shellui.json + ctx.Status("Creating configuration..."); + var config = new ShellUIConfig + { + Style = style, + ComponentsPath = "Components/UI", + ProjectType = projectInfo.ProjectType, + Tailwind = new TailwindConfig + { + Enabled = true, + Version = "4.1.0", + CssPath = "wwwroot/app.css" + } + }; + + var json = JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(configPath, json); + AnsiConsole.MarkupLine($"[green]Created:[/] shellui.json"); + + // Step 4: Create _Imports.razor if it doesn't exist + ctx.Status("Setting up imports..."); + var importsPath = Path.Combine(Directory.GetCurrentDirectory(), "Components", "_Imports.razor"); + if (File.Exists(importsPath)) + { + var importsContent = File.ReadAllText(importsPath); + var usingStatement = $"@using {projectInfo.RootNamespace}.Components.UI"; + + if (!importsContent.Contains(usingStatement)) + { + File.AppendAllText(importsPath, $"\n{usingStatement}\n"); + AnsiConsole.MarkupLine($"[green]Updated:[/] Components/_Imports.razor"); + } + } + }); + + AnsiConsole.MarkupLine("\n[green]ShellUI initialized successfully![/]"); + AnsiConsole.MarkupLine("\n[blue]Next steps:[/]"); + AnsiConsole.MarkupLine(" [dim]1. Add components:[/] dotnet shellui add button"); + AnsiConsole.MarkupLine(" [dim]2. Browse all:[/] dotnet shellui list"); + } +} + From 23844f3f70d36a4bf7e56ee26e59c26816793e61 Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 13 Oct 2025 22:58:54 +0200 Subject: [PATCH 06/45] feat: Add ProjectDetector for automatic project detection in ShellUI CLI --- src/ShellUI.CLI/Services/ProjectDetector.cs | 80 +++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/ShellUI.CLI/Services/ProjectDetector.cs diff --git a/src/ShellUI.CLI/Services/ProjectDetector.cs b/src/ShellUI.CLI/Services/ProjectDetector.cs new file mode 100644 index 0000000..27a18e3 --- /dev/null +++ b/src/ShellUI.CLI/Services/ProjectDetector.cs @@ -0,0 +1,80 @@ +using ShellUI.Core.Models; +using System.Xml.Linq; + +namespace ShellUI.CLI.Services; + +public class ProjectDetector +{ + public static ProjectInfo DetectProject() + { + var csprojFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj"); + + if (csprojFiles.Length == 0) + { + throw new Exception("No .csproj file found in current directory. Please run this command from your Blazor project root."); + } + + var csprojPath = csprojFiles[0]; + var projectName = Path.GetFileNameWithoutExtension(csprojPath); + + var doc = XDocument.Load(csprojPath); + var sdk = doc.Root?.Attribute("Sdk")?.Value ?? ""; + + var projectType = DetectProjectType(doc, csprojPath); + var rootNamespace = DetectRootNamespace(doc, projectName); + + return new ProjectInfo + { + ProjectPath = csprojPath, + ProjectName = projectName, + RootNamespace = rootNamespace, + ProjectType = projectType + }; + } + + private static ProjectType DetectProjectType(XDocument doc, string csprojPath) + { + var projectDir = Path.GetDirectoryName(csprojPath) ?? ""; + + // Check for Program.cs patterns + var programCsPath = Path.Combine(projectDir, "Program.cs"); + if (File.Exists(programCsPath)) + { + var programContent = File.ReadAllText(programCsPath); + + if (programContent.Contains("AddInteractiveServerComponents")) + return ProjectType.BlazorServer; + + if (programContent.Contains("AddInteractiveWebAssemblyComponents")) + return ProjectType.BlazorWebAssembly; + + if (programContent.Contains("AddRazorComponents")) + return ProjectType.BlazorServerSideRendering; + } + + // Fallback: check SDK + var sdk = doc.Root?.Attribute("Sdk")?.Value ?? ""; + if (sdk.Contains("Web")) + return ProjectType.BlazorServerSideRendering; + + return ProjectType.Unknown; + } + + private static string DetectRootNamespace(XDocument doc, string projectName) + { + var rootNamespace = doc.Root? + .Descendants("RootNamespace") + .FirstOrDefault()?.Value; + + return rootNamespace ?? projectName; + } +} + +public class ProjectInfo +{ + public required string ProjectPath { get; set; } + public required string ProjectName { get; set; } + public required string RootNamespace { get; set; } + public ProjectType ProjectType { get; set; } +} + From 1fe66ed9e0cc136240053fb5c33d3072a2945a88 Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 13 Oct 2025 23:00:24 +0200 Subject: [PATCH 07/45] feat: Refactor BlazorInteractiveServer project structure --- .../BlazorInteractiveServer.csproj | 5 +- .../Components/UI/Button.razor | 105 ++++++++++++++++++ .../Components/_Imports.razor | 2 +- NET9/BlazorInteractiveServer/shellui.json | 20 ++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Button.razor create mode 100644 NET9/BlazorInteractiveServer/shellui.json diff --git a/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj index e61a160..c9778e7 100644 --- a/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj +++ b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj @@ -6,8 +6,9 @@ enable - + + diff --git a/NET9/BlazorInteractiveServer/Components/UI/Button.razor b/NET9/BlazorInteractiveServer/Components/UI/Button.razor new file mode 100644 index 0000000..27c4f3f --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Button.razor @@ -0,0 +1,105 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string Size { get; set; } = "md"; + + [Parameter] + public string Type { get; set; } = "button"; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public bool IsLoading { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + // Base styles + "inline-flex", + "items-center", + "justify-center", + "gap-2", + "whitespace-nowrap", + "rounded-md", + "text-sm", + "font-medium", + "ring-offset-background", + "transition-colors", + + // Focus styles + "focus-visible:outline-none", + "focus-visible:ring-2", + "focus-visible:ring-ring", + "focus-visible:ring-offset-2", + + // Disabled styles + "disabled:pointer-events-none", + "disabled:opacity-50" + }; + + // Variant styles + classes.Add(Variant.ToLower() switch + { + "default" => "bg-primary text-primary-foreground hover:bg-primary/90", + "destructive" => "bg-destructive text-destructive-foreground hover:bg-destructive/90", + "outline" => "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + "secondary" => "bg-secondary text-secondary-foreground hover:bg-secondary/80", + "ghost" => "hover:bg-accent hover:text-accent-foreground", + "link" => "text-primary underline-offset-4 hover:underline", + _ => "bg-primary text-primary-foreground hover:bg-primary/90" + }); + + // Size styles + classes.Add(Size.ToLower() switch + { + "sm" => "h-9 rounded-md px-3", + "md" => "h-10 px-4 py-2", + "lg" => "h-11 rounded-md px-8", + "icon" => "h-10 w-10", + _ => "h-10 px-4 py-2" + }); + + return string.Join(" ", classes); + } + + private async Task HandleClick(MouseEventArgs args) + { + if (!Disabled && !IsLoading) + { + await OnClick.InvokeAsync(args); + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/_Imports.razor b/NET9/BlazorInteractiveServer/Components/_Imports.razor index adb94bc..a7cb58e 100644 --- a/NET9/BlazorInteractiveServer/Components/_Imports.razor +++ b/NET9/BlazorInteractiveServer/Components/_Imports.razor @@ -9,4 +9,4 @@ @using BlazorInteractiveServer @using BlazorInteractiveServer.Components -@using ShellUI.Components +@using BlazorInteractiveServer.Components.UI diff --git a/NET9/BlazorInteractiveServer/shellui.json b/NET9/BlazorInteractiveServer/shellui.json new file mode 100644 index 0000000..d575095 --- /dev/null +++ b/NET9/BlazorInteractiveServer/shellui.json @@ -0,0 +1,20 @@ +{ + "Schema": "https://shellui.dev/schema.json", + "Style": "default", + "ComponentsPath": "Components/UI", + "Tailwind": { + "Enabled": true, + "Version": "4.1.0", + "ConfigPath": "tailwind.config.js", + "CssPath": "wwwroot/app.css" + }, + "InstalledComponents": [ + { + "Name": "button", + "Version": "0.1.0", + "InstalledAt": "2025-10-12T02:00:23.4449527Z", + "IsCustomized": false + } + ], + "ProjectType": 1 +} \ No newline at end of file From 8774b3893a764e6550672097c3b4e0f91c7ae35c Mon Sep 17 00:00:00 2001 From: Shewart Date: Mon, 13 Oct 2025 23:01:01 +0200 Subject: [PATCH 08/45] feat: Enhance ShellUI CLI with error handling and component installation --- src/ShellUI.CLI/Program.cs | 70 +++++++------------------------------- 1 file changed, 12 insertions(+), 58 deletions(-) diff --git a/src/ShellUI.CLI/Program.cs b/src/ShellUI.CLI/Program.cs index 6f46103..4b88f9f 100644 --- a/src/ShellUI.CLI/Program.cs +++ b/src/ShellUI.CLI/Program.cs @@ -2,6 +2,7 @@ using Spectre.Console; using ShellUI.Templates; using ShellUI.Core.Models; +using ShellUI.CLI.Services; namespace ShellUI.CLI; @@ -41,22 +42,15 @@ static Command CreateInitCommand() command.SetHandler((force, style) => { - AnsiConsole.Write(new FigletText("ShellUI").Color(Color.Blue)); - AnsiConsole.MarkupLine("[green]Initializing ShellUI...[/]"); - AnsiConsole.MarkupLine($"[dim]Style: {style}[/]"); - - if (force) + try { - AnsiConsole.MarkupLine("[yellow]Force mode enabled[/]"); + AnsiConsole.Write(new FigletText("ShellUI").Color(Color.Blue)); + InitService.Initialize(style, force); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); } - - AnsiConsole.MarkupLine("\n[yellow]Coming soon in Milestone 1![/]"); - AnsiConsole.MarkupLine("[dim]This will:[/]"); - AnsiConsole.MarkupLine("[dim] - Detect your project type (Server/WASM/SSR)[/]"); - AnsiConsole.MarkupLine("[dim] - Create Components/UI folder[/]"); - AnsiConsole.MarkupLine("[dim] - Download Tailwind standalone CLI (no Node.js!)[/]"); - AnsiConsole.MarkupLine("[dim] - Create shellui.json config[/]"); - AnsiConsole.MarkupLine("[dim] - Set up Tailwind CSS v4[/]"); }, forceOption, styleOption); return command; @@ -81,54 +75,14 @@ static Command CreateAddCommand() command.SetHandler((components, force) => { - AnsiConsole.MarkupLine("[green]Adding components:[/]\n"); - - // Parse comma-separated components - var componentList = new List(); - foreach (var comp in components) - { - componentList.AddRange(comp.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); - } - - var table = new Table(); - table.AddColumn("Component"); - table.AddColumn("Status"); - table.AddColumn("Version"); - - foreach (var componentName in componentList) + try { - var exists = ComponentRegistry.Exists(componentName); - if (exists) - { - var metadata = ComponentRegistry.GetMetadata(componentName); - table.AddRow( - componentName, - "[yellow]Implementation pending[/]", - $"[dim]{metadata?.Version ?? "0.1.0"}[/]" - ); - } - else - { - table.AddRow( - componentName, - "[red]Not found[/]", - "[dim]-[/]" - ); - } + ComponentInstaller.InstallComponents(components, force); } - - AnsiConsole.Write(table); - - if (force) + catch (Exception ex) { - AnsiConsole.MarkupLine("\n[yellow]Force mode: Will overwrite existing components[/]"); + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); } - - AnsiConsole.MarkupLine("\n[dim]Multiple component syntax supported:[/]"); - AnsiConsole.MarkupLine("[dim] dotnet shellui add button card alert[/]"); - AnsiConsole.MarkupLine("[dim] dotnet shellui add button,card,alert[/]"); - - AnsiConsole.MarkupLine("\n[yellow]Component copying implementation coming soon![/]"); }, componentsArg, forceOption); return command; From 2ba6261a45974ef36b8d63dbbc1a47d547871d32 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:14:15 +0200 Subject: [PATCH 09/45] feat: Add ShellUI.targets for Tailwind CSS build and clean targets --- .../Build/ShellUI.targets | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 NET9/BlazorInteractiveServer/Build/ShellUI.targets diff --git a/NET9/BlazorInteractiveServer/Build/ShellUI.targets b/NET9/BlazorInteractiveServer/Build/ShellUI.targets new file mode 100644 index 0000000..de3163d --- /dev/null +++ b/NET9/BlazorInteractiveServer/Build/ShellUI.targets @@ -0,0 +1,23 @@ + + + + $(MSBuildProjectDirectory)\.shellui\bin + $(ShellUIBinPath)\tailwindcss.exe + $(ShellUIBinPath)/tailwindcss + $(MSBuildProjectDirectory)\wwwroot\input.css + $(MSBuildProjectDirectory)\wwwroot\app.css + --minify + + + + + + + + + + + + + + \ No newline at end of file From 95bd64fb5b6d176eee2f5e7f3630ada44ce1290d Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:14:27 +0200 Subject: [PATCH 10/45] feat: Add theme toggle component to Home.razor for enhanced user experience --- NET9/BlazorInteractiveServer/Components/Pages/Home.razor | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor index 2b1ea3c..e64c31f 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor @@ -3,6 +3,11 @@ ShellUI - Button Demo
+ +
+ +
+

ShellUI Button Component

Beautiful, accessible buttons inspired by shadcn/ui

From c54b25402af87337f6ff5e8f20797e98080d9846 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:14:40 +0200 Subject: [PATCH 11/45] feat: Implement ThemeToggle component for user theme switching functionality --- .../Components/UI/ThemeToggle.razor | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor diff --git a/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor new file mode 100644 index 0000000..7a35f4c --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor @@ -0,0 +1,63 @@ +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime + + + +@code { + private bool _isDark = true; + + protected override async Task OnInitializedAsync() + { + try + { + var theme = await JSRuntime.InvokeAsync("localStorage.getItem", "theme"); + _isDark = string.IsNullOrEmpty(theme) ? true : theme == "dark"; + } + catch + { + _isDark = true; + } + } + + private async Task ToggleTheme() + { + _isDark = !_isDark; + var theme = _isDark ? "dark" : "light"; + + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "theme", theme); + + if (_isDark) + { + await JSRuntime.InvokeVoidAsync("eval", "document.documentElement.classList.add('dark')"); + } + else + { + await JSRuntime.InvokeVoidAsync("eval", "document.documentElement.classList.remove('dark')"); + } + } + catch + { + // JSRuntime not available (pre-render) + } + } +} From 4c3c6d54ed2707d3e0c7aac42ed9236af9c3c9b8 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:15:26 +0200 Subject: [PATCH 12/45] feat: Refactor InitService to support asynchronous initialization and enhance ShellUI project setup --- src/ShellUI.CLI/Services/InitService.cs | 222 ++++++++++++++++++------ 1 file changed, 169 insertions(+), 53 deletions(-) diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs index f3feb0a..fd0d152 100644 --- a/src/ShellUI.CLI/Services/InitService.cs +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -1,4 +1,5 @@ using ShellUI.Core.Models; +using ShellUI.Templates; using System.Text.Json; using Spectre.Console; @@ -6,7 +7,7 @@ namespace ShellUI.CLI.Services; public class InitService { - public static void Initialize(string style, bool force) + public static async Task InitializeAsync(string style, bool force) { var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); @@ -17,64 +18,179 @@ public static void Initialize(string style, bool force) return; } - AnsiConsole.Status() - .Start("Initializing ShellUI...", ctx => + // Step 1: Detect project + AnsiConsole.MarkupLine("[cyan]Initializing ShellUI...[/]"); + var projectInfo = ProjectDetector.DetectProject(); + AnsiConsole.MarkupLine($"[green]Detected:[/] {projectInfo.ProjectType}"); + AnsiConsole.MarkupLine($"[dim]Project: {projectInfo.ProjectName}[/]"); + AnsiConsole.MarkupLine($"[dim]Namespace: {projectInfo.RootNamespace}[/]"); + + // Step 2: Create Components/UI folder + AnsiConsole.MarkupLine("[cyan]Creating component folders...[/]"); + var componentsPath = Path.Combine(Directory.GetCurrentDirectory(), "Components", "UI"); + Directory.CreateDirectory(componentsPath); + AnsiConsole.MarkupLine($"[green]Created:[/] Components/UI/"); + + // Step 3: Create shellui.json + AnsiConsole.MarkupLine("[cyan]Creating configuration...[/]"); + var config = new ShellUIConfig + { + Style = style, + ComponentsPath = "Components/UI", + ProjectType = projectInfo.ProjectType, + Tailwind = new TailwindConfig + { + Enabled = true, + Version = "4.1.0", + CssPath = "wwwroot/app.css" + } + }; + + var json = JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(configPath, json); + AnsiConsole.MarkupLine($"[green]Created:[/] shellui.json"); + + // Step 4: Create _Imports.razor if it doesn't exist + AnsiConsole.MarkupLine("[cyan]Setting up imports...[/]"); + var importsPath = Path.Combine(Directory.GetCurrentDirectory(), "Components", "_Imports.razor"); + if (File.Exists(importsPath)) + { + var importsContent = File.ReadAllText(importsPath); + var usingStatement = $"@using {projectInfo.RootNamespace}.Components.UI"; + + if (!importsContent.Contains(usingStatement)) { - // Step 1: Detect project - ctx.Status("Detecting project type..."); - var projectInfo = ProjectDetector.DetectProject(); - AnsiConsole.MarkupLine($"[green]Detected:[/] {projectInfo.ProjectType}"); - AnsiConsole.MarkupLine($"[dim]Project: {projectInfo.ProjectName}[/]"); - AnsiConsole.MarkupLine($"[dim]Namespace: {projectInfo.RootNamespace}[/]"); - - // Step 2: Create Components/UI folder - ctx.Status("Creating component folders..."); - var componentsPath = Path.Combine(Directory.GetCurrentDirectory(), "Components", "UI"); - Directory.CreateDirectory(componentsPath); - AnsiConsole.MarkupLine($"[green]Created:[/] Components/UI/"); - - // Step 3: Create shellui.json - ctx.Status("Creating configuration..."); - var config = new ShellUIConfig - { - Style = style, - ComponentsPath = "Components/UI", - ProjectType = projectInfo.ProjectType, - Tailwind = new TailwindConfig - { - Enabled = true, - Version = "4.1.0", - CssPath = "wwwroot/app.css" - } - }; - - var json = JsonSerializer.Serialize(config, new JsonSerializerOptions - { - WriteIndented = true - }); - File.WriteAllText(configPath, json); - AnsiConsole.MarkupLine($"[green]Created:[/] shellui.json"); - - // Step 4: Create _Imports.razor if it doesn't exist - ctx.Status("Setting up imports..."); - var importsPath = Path.Combine(Directory.GetCurrentDirectory(), "Components", "_Imports.razor"); - if (File.Exists(importsPath)) - { - var importsContent = File.ReadAllText(importsPath); - var usingStatement = $"@using {projectInfo.RootNamespace}.Components.UI"; - - if (!importsContent.Contains(usingStatement)) - { - File.AppendAllText(importsPath, $"\n{usingStatement}\n"); - AnsiConsole.MarkupLine($"[green]Updated:[/] Components/_Imports.razor"); - } - } - }); + File.AppendAllText(importsPath, $"\n{usingStatement}\n"); + AnsiConsole.MarkupLine($"[green]Updated:[/] Components/_Imports.razor"); + } + } + + // Step 5: Download Tailwind CLI (no Node.js required!) + AnsiConsole.MarkupLine("[cyan]Downloading Tailwind CSS standalone CLI...[/]"); + var tailwindPath = await TailwindDownloader.EnsureTailwindCliAsync(Directory.GetCurrentDirectory()); + + // Step 6: Create CSS files + AnsiConsole.MarkupLine("[cyan]Creating CSS files...[/]"); + var wwwrootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); + Directory.CreateDirectory(wwwrootPath); + + // Create input.css with design tokens + var inputCssPath = Path.Combine(wwwrootPath, "input.css"); + File.WriteAllText(inputCssPath, CssTemplates.InputCss); + AnsiConsole.MarkupLine($"[green]Created:[/] wwwroot/input.css"); + + // Create placeholder app.css (will be generated by Tailwind) + var appCssPath = Path.Combine(wwwrootPath, "app.css"); + File.WriteAllText(appCssPath, CssTemplates.AppCss); + AnsiConsole.MarkupLine($"[green]Created:[/] wwwroot/app.css"); + + // Create tailwind.config.js + var tailwindConfigPath = Path.Combine(Directory.GetCurrentDirectory(), "tailwind.config.js"); + File.WriteAllText(tailwindConfigPath, CssTemplates.TailwindConfigJs); + AnsiConsole.MarkupLine($"[green]Created:[/] tailwind.config.js"); + + // Step 7: Create MSBuild targets file + AnsiConsole.MarkupLine("[cyan]Setting up MSBuild integration...[/]"); + var buildPath = Path.Combine(Directory.GetCurrentDirectory(), "Build"); + Directory.CreateDirectory(buildPath); + + var targetsPath = Path.Combine(buildPath, "ShellUI.targets"); + var targetsContent = GetTargetsFileContent(); + File.WriteAllText(targetsPath, targetsContent); + AnsiConsole.MarkupLine($"[green]Created:[/] Build/ShellUI.targets"); + + // Update .csproj to import targets + await UpdateProjectFileAsync(projectInfo.ProjectPath, targetsPath); + AnsiConsole.MarkupLine($"[green]Updated:[/] {Path.GetFileName(projectInfo.ProjectPath)}"); + + // Step 8: Run initial Tailwind build + AnsiConsole.MarkupLine("[cyan]Building Tailwind CSS...[/]"); + await RunTailwindBuildAsync(tailwindPath, inputCssPath, appCssPath); + AnsiConsole.MarkupLine($"[green]Built:[/] Tailwind CSS"); AnsiConsole.MarkupLine("\n[green]ShellUI initialized successfully![/]"); AnsiConsole.MarkupLine("\n[blue]Next steps:[/]"); AnsiConsole.MarkupLine(" [dim]1. Add components:[/] dotnet shellui add button"); AnsiConsole.MarkupLine(" [dim]2. Browse all:[/] dotnet shellui list"); } + + private static async Task RunTailwindBuildAsync(string tailwindPath, string inputPath, string outputPath) + { + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = tailwindPath, + Arguments = $"-i \"{inputPath}\" -o \"{outputPath}\" --minify", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = System.Diagnostics.Process.Start(startInfo); + if (process == null) + { + throw new Exception("Failed to start Tailwind CSS process"); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + var error = await process.StandardError.ReadToEndAsync(); + throw new Exception($"Tailwind CSS build failed: {error}"); + } + } + + private static string GetTargetsFileContent() + { + return @" + + + $(MSBuildProjectDirectory)\.shellui\bin + $(ShellUIBinPath)\tailwindcss.exe + $(ShellUIBinPath)/tailwindcss + $(MSBuildProjectDirectory)\wwwroot\input.css + $(MSBuildProjectDirectory)\wwwroot\app.css + --minify + + + + + + + + + + + + + +"; + } + + private static async Task UpdateProjectFileAsync(string projectFilePath, string targetsPath) + { + var content = await File.ReadAllTextAsync(projectFilePath); + var targetsImport = $" "; + + // Check if import already exists + if (content.Contains(targetsImport) || content.Contains("ShellUI.targets")) + { + return; + } + + // Insert before closing tag + var closingTag = ""; + var insertIndex = content.LastIndexOf(closingTag); + + if (insertIndex > 0) + { + content = content.Insert(insertIndex, targetsImport + Environment.NewLine + Environment.NewLine); + await File.WriteAllTextAsync(projectFilePath, content); + } + } } From 64574ea59fe3127867c9912621ae938731a331d7 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:16:05 +0200 Subject: [PATCH 13/45] feat: Update app.css to integrate Tailwind CSS v4.1.0 and enhance styling utilities --- NET9/BlazorInteractiveServer/wwwroot/app.css | 358 +++++++++++-------- 1 file changed, 207 insertions(+), 151 deletions(-) diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index f7f140a..2ebef71 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -1,152 +1,208 @@ -/* ShellUI - Tailwind CSS v4 Setup */ - -/* We're using Tailwind Play CDN from the HTML head, but we define our theme here */ - -:root { - /* Light mode colors */ - --color-background: 255 255 255; - --color-foreground: 15 23 42; - --color-card: 255 255 255; - --color-card-foreground: 15 23 42; - --color-popover: 255 255 255; - --color-popover-foreground: 15 23 42; - --color-primary: 59 130 246; - --color-primary-foreground: 255 255 255; - --color-secondary: 100 116 139; - --color-secondary-foreground: 255 255 255; - --color-muted: 241 245 249; - --color-muted-foreground: 100 116 139; - --color-accent: 241 245 249; - --color-accent-foreground: 15 23 42; - --color-destructive: 239 68 68; - --color-destructive-foreground: 255 255 255; - --color-border: 226 232 240; - --color-input: 226 232 240; - --color-ring: 59 130 246; - --radius: 0.5rem; -} - -/* Dark mode colors */ -.dark { - --color-background: 15 23 42; - --color-foreground: 248 250 252; - --color-card: 15 23 42; - --color-card-foreground: 248 250 252; - --color-popover: 15 23 42; - --color-popover-foreground: 248 250 252; - --color-primary: 59 130 246; - --color-primary-foreground: 255 255 255; - --color-secondary: 71 85 105; - --color-secondary-foreground: 248 250 252; - --color-muted: 30 41 59; - --color-muted-foreground: 148 163 184; - --color-accent: 30 41 59; - --color-accent-foreground: 248 250 252; - --color-destructive: 220 38 38; - --color-destructive-foreground: 255 255 255; - --color-border: 30 41 59; - --color-input: 30 41 59; - --color-ring: 59 130 246; -} - -/* Apply colors using rgb() */ -.bg-background { - background-color: rgb(var(--color-background)); -} - -.text-foreground { - color: rgb(var(--color-foreground)); -} - -.bg-primary { - background-color: rgb(var(--color-primary)); -} - -.text-primary-foreground { - color: rgb(var(--color-primary-foreground)); -} - -.bg-secondary { - background-color: rgb(var(--color-secondary)); -} - -.text-secondary-foreground { - color: rgb(var(--color-secondary-foreground)); -} - -.bg-destructive { - background-color: rgb(var(--color-destructive)); -} - -.text-destructive-foreground { - color: rgb(var(--color-destructive-foreground)); -} - -.bg-muted { - background-color: rgb(var(--color-muted)); -} - -.text-muted-foreground { - color: rgb(var(--color-muted-foreground)); -} - -.bg-accent { - background-color: rgb(var(--color-accent)); -} - -.text-accent-foreground { - color: rgb(var(--color-accent-foreground)); -} - -.border-input { - border-color: rgb(var(--color-border)); -} - -.ring-ring { - --tw-ring-color: rgb(var(--color-ring)); -} - -/* Hover variants */ -.hover\:bg-primary\/90:hover { - background-color: rgb(var(--color-primary) / 0.9); -} - -.hover\:bg-secondary\/80:hover { - background-color: rgb(var(--color-secondary) / 0.8); -} - -.hover\:bg-destructive\/90:hover { - background-color: rgb(var(--color-destructive) / 0.9); -} - -.hover\:bg-accent:hover { - background-color: rgb(var(--color-accent)); -} - -.hover\:text-accent-foreground:hover { - color: rgb(var(--color-accent-foreground)); -} - -/* Blazor validation styles */ -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid rgb(var(--color-destructive)); -} - -.validation-message { - color: rgb(var(--color-destructive)); -} - -/* Blazor error boundary */ -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - -.blazor-error-boundary::after { - content: "An error has occurred." +/*! tailwindcss v4.1.0 | MIT License | https://tailwindcss.com */ +@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + @layer base { + *, ::before, ::after, ::backdrop { + --tw-border-style: solid; + --tw-outline-style: solid; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + } + } +} +.static { + position: static; +} +.container { + width: 100%; +} +.mx-auto { + margin-inline: auto; +} +.flex { + display: flex; +} +.inline-flex { + display: inline-flex; +} +.min-h-screen { + min-height: 100vh; +} +.flex-wrap { + flex-wrap: wrap; +} +.items-center { + align-items: center; +} +.justify-between { + justify-content: space-between; +} +.justify-center { + justify-content: center; +} +.justify-end { + justify-content: flex-end; +} +.border { + border-style: var(--tw-border-style); + border-width: 1px; +} +.border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; +} +.border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; +} +.text-center { + text-align: center; +} +.whitespace-nowrap { + white-space: nowrap; +} +.underline-offset-4 { + text-underline-offset: 4px; +} +.opacity-25 { + opacity: 25%; +} +.opacity-75 { + opacity: 75%; +} +.outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; +} +.transition-colors { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, ease); + transition-duration: var(--tw-duration, 0s); +} +.hover\:underline { + &:hover { + @media (hover: hover) { + text-decoration-line: underline; + } + } +} +.focus-visible\:ring-1 { + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } +} +.focus-visible\:ring-2 { + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } +} +.focus-visible\:ring-offset-2 { + &:focus-visible { + --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); + } +} +.focus-visible\:outline-none { + &:focus-visible { + --tw-outline-style: none; + outline-style: none; + } +} +.disabled\:pointer-events-none { + &:disabled { + pointer-events: none; + } +} +.disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; } From 20bf9d9eae098aa447557e1fcd64e46853586106 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:16:24 +0200 Subject: [PATCH 14/45] feat: Add ThemeToggleTemplate for customizable theme switching in ShellUI --- .../Templates/ThemeToggleTemplate.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs diff --git a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs new file mode 100644 index 0000000..cf782cc --- /dev/null +++ b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs @@ -0,0 +1,157 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class ThemeToggleTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "theme-toggle", + DisplayName = "Theme Toggle", + Description = "Toggle between light and dark mode", + Category = ComponentCategory.Utility, + Version = "0.1.0", + FilePath = "ThemeToggle.razor", + Dependencies = new List() + }; + + public static string Content => @"@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime + + + +@code { + private bool _isDark = true; + + protected override async Task OnInitializedAsync() + { + try + { + var theme = await JSRuntime.InvokeAsync(""localStorage.getItem"", ""theme""); + _isDark = string.IsNullOrEmpty(theme) ? true : theme == ""dark""; + } + catch + { + _isDark = true; + } + } + + private async Task ToggleTheme() + { + _isDark = !_isDark; + var theme = _isDark ? ""dark"" : ""light""; + + try + { + await JSRuntime.InvokeVoidAsync(""localStorage.setItem"", ""theme"", theme); + + if (_isDark) + { + await JSRuntime.InvokeVoidAsync(""eval"", ""document.documentElement.classList.add('dark')""); + } + else + { + await JSRuntime.InvokeVoidAsync(""eval"", ""document.documentElement.classList.remove('dark')""); + } + } + catch + { + // JSRuntime not available (pre-render) + } + } +} +"; + + public static string ThemeServiceContent => @"using Microsoft.JSInterop; + +namespace YourProjectNamespace.Services; + +public interface IThemeService +{ + Task GetThemeAsync(); + Task SetThemeAsync(string theme); + Task ToggleThemeAsync(); + Task InitializeThemeAsync(); +} + +public class ThemeService : IThemeService +{ + private readonly IJSRuntime _jsRuntime; + private string _currentTheme = ""dark""; + + public ThemeService(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + public async Task GetThemeAsync() + { + try + { + var theme = await _jsRuntime.InvokeAsync(""localStorage.getItem"", ""theme""); + _currentTheme = string.IsNullOrEmpty(theme) ? ""dark"" : theme; + return _currentTheme; + } + catch + { + return _currentTheme; + } + } + + public async Task SetThemeAsync(string theme) + { + _currentTheme = theme; + + try + { + await _jsRuntime.InvokeVoidAsync(""localStorage.setItem"", ""theme"", theme); + + if (theme == ""dark"") + { + await _jsRuntime.InvokeVoidAsync(""eval"", ""document.documentElement.classList.add('dark')""); + } + else + { + await _jsRuntime.InvokeVoidAsync(""eval"", ""document.documentElement.classList.remove('dark')""); + } + } + catch + { + // JSRuntime not available (pre-render) + } + } + + public async Task ToggleThemeAsync() + { + var currentTheme = await GetThemeAsync(); + var newTheme = currentTheme == ""dark"" ? ""light"" : ""dark""; + await SetThemeAsync(newTheme); + } + + public async Task InitializeThemeAsync() + { + var theme = await GetThemeAsync(); + await SetThemeAsync(theme); + } +} +"; +} + From aa666bee2e1087c46c938bf6d7c0988b591fba71 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:16:41 +0200 Subject: [PATCH 15/45] feat: Integrate Tailwind CSS configuration and update theme toggle component in BlazorInteractiveServer --- .../BlazorInteractiveServer.csproj | 2 ++ .../Components/App.razor | 5 +---- NET9/BlazorInteractiveServer/shellui.json | 4 ++-- NET9/BlazorInteractiveServer/tailwind.config.js | 11 +++++++++++ NET9/BlazorInteractiveServer/wwwroot/input.css | 17 +++++++++++++++++ 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 NET9/BlazorInteractiveServer/tailwind.config.js create mode 100644 NET9/BlazorInteractiveServer/wwwroot/input.css diff --git a/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj index c9778e7..b793beb 100644 --- a/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj +++ b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj @@ -11,4 +11,6 @@ --> + + diff --git a/NET9/BlazorInteractiveServer/Components/App.razor b/NET9/BlazorInteractiveServer/Components/App.razor index 9b63d78..4330148 100644 --- a/NET9/BlazorInteractiveServer/Components/App.razor +++ b/NET9/BlazorInteractiveServer/Components/App.razor @@ -6,10 +6,7 @@ - - - - + diff --git a/NET9/BlazorInteractiveServer/shellui.json b/NET9/BlazorInteractiveServer/shellui.json index d575095..cec0e2c 100644 --- a/NET9/BlazorInteractiveServer/shellui.json +++ b/NET9/BlazorInteractiveServer/shellui.json @@ -10,9 +10,9 @@ }, "InstalledComponents": [ { - "Name": "button", + "Name": "theme-toggle", "Version": "0.1.0", - "InstalledAt": "2025-10-12T02:00:23.4449527Z", + "InstalledAt": "2025-10-13T22:11:02.8329485Z", "IsCustomized": false } ], diff --git a/NET9/BlazorInteractiveServer/tailwind.config.js b/NET9/BlazorInteractiveServer/tailwind.config.js new file mode 100644 index 0000000..b1ac5ee --- /dev/null +++ b/NET9/BlazorInteractiveServer/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './Components/**/*.{razor,html,cshtml}', + './Pages/**/*.{razor,html,cshtml}', + ], + darkMode: 'class', + theme: { + extend: {}, + }, +} diff --git a/NET9/BlazorInteractiveServer/wwwroot/input.css b/NET9/BlazorInteractiveServer/wwwroot/input.css new file mode 100644 index 0000000..a9e95b3 --- /dev/null +++ b/NET9/BlazorInteractiveServer/wwwroot/input.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + * ShellUI - Customize your theme here! + * + * Add your color variables and theme customizations below. + * These will be processed by the Tailwind standalone CLI. + * + * Example: + * + * @theme { + * --color-primary: #3b82f6; + * --color-secondary: #64748b; + * } + */ From 342a00eb4a6626486ec1597d1fe5e6254d243586 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:16:56 +0200 Subject: [PATCH 16/45] feat: Add theme-toggle component to ComponentRegistry and introduce CSS templates for Tailwind integration --- src/ShellUI.Templates/Build/ShellUI.targets | 33 +++++++++++++++ src/ShellUI.Templates/ComponentRegistry.cs | 4 +- src/ShellUI.Templates/CssTemplates.cs | 47 +++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/ShellUI.Templates/Build/ShellUI.targets create mode 100644 src/ShellUI.Templates/CssTemplates.cs diff --git a/src/ShellUI.Templates/Build/ShellUI.targets b/src/ShellUI.Templates/Build/ShellUI.targets new file mode 100644 index 0000000..0926110 --- /dev/null +++ b/src/ShellUI.Templates/Build/ShellUI.targets @@ -0,0 +1,33 @@ + + + + $(MSBuildProjectDirectory)\.shellui\bin + $(ShellUIBinPath)\tailwindcss.exe + $(ShellUIBinPath)/tailwindcss + $(MSBuildProjectDirectory)\wwwroot\input.css + $(MSBuildProjectDirectory)\wwwroot\app.css + --minify + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index 4f7bacf..77b3766 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -7,7 +7,8 @@ public static class ComponentRegistry { public static readonly Dictionary Components = new() { - { "button", ButtonTemplate.Metadata } + { "button", ButtonTemplate.Metadata }, + { "theme-toggle", ThemeToggleTemplate.Metadata } }; public static string? GetComponentContent(string componentName) @@ -15,6 +16,7 @@ public static class ComponentRegistry return componentName.ToLower() switch { "button" => ButtonTemplate.Content, + "theme-toggle" => ThemeToggleTemplate.Content, _ => null }; } diff --git a/src/ShellUI.Templates/CssTemplates.cs b/src/ShellUI.Templates/CssTemplates.cs new file mode 100644 index 0000000..d74a68e --- /dev/null +++ b/src/ShellUI.Templates/CssTemplates.cs @@ -0,0 +1,47 @@ +namespace ShellUI.Templates; + +public static class CssTemplates +{ + public static string InputCss => @"@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + * ShellUI - Customize your theme here! + * + * Add your color variables and theme customizations below. + * These will be processed by the Tailwind standalone CLI. + * + * Example: + * + * @theme { + * --color-primary: #3b82f6; + * --color-secondary: #64748b; + * } + */ +"; + + public static string AppCss => @"/* + * ShellUI - Blazor Component Library + * Generated by: dotnet shellui init + * + * This file is auto-generated by Tailwind CLI. + * Do NOT edit this file directly - edit wwwroot/input.css instead. + */ + +/* Tailwind will generate the final CSS here */ +"; + + public static string TailwindConfigJs => @"/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './Components/**/*.{razor,html,cshtml}', + './Pages/**/*.{razor,html,cshtml}', + ], + darkMode: 'class', + theme: { + extend: {}, + }, +} +"; +} \ No newline at end of file From 25e44f8c1082366d8f1324974599735681279cf1 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:17:11 +0200 Subject: [PATCH 17/45] feat: Implement asynchronous initialization in ShellUI CLI and add TailwindDownloader service for CLI integration --- src/ShellUI.CLI/Program.cs | 4 +- .../Services/TailwindDownloader.cs | 112 ++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/ShellUI.CLI/Services/TailwindDownloader.cs diff --git a/src/ShellUI.CLI/Program.cs b/src/ShellUI.CLI/Program.cs index 4b88f9f..2989544 100644 --- a/src/ShellUI.CLI/Program.cs +++ b/src/ShellUI.CLI/Program.cs @@ -40,12 +40,12 @@ static Command CreateInitCommand() command.AddOption(forceOption); command.AddOption(styleOption); - command.SetHandler((force, style) => + command.SetHandler(async (force, style) => { try { AnsiConsole.Write(new FigletText("ShellUI").Color(Color.Blue)); - InitService.Initialize(style, force); + await InitService.InitializeAsync(style, force); } catch (Exception ex) { diff --git a/src/ShellUI.CLI/Services/TailwindDownloader.cs b/src/ShellUI.CLI/Services/TailwindDownloader.cs new file mode 100644 index 0000000..890b0ca --- /dev/null +++ b/src/ShellUI.CLI/Services/TailwindDownloader.cs @@ -0,0 +1,112 @@ +using System.IO.Compression; +using System.Runtime.InteropServices; +using Spectre.Console; + +namespace ShellUI.CLI.Services; + +public class TailwindDownloader +{ + private const string TailwindVersion = "v4.1.0"; + private const string BaseUrl = "https://github.com/tailwindlabs/tailwindcss/releases/download"; + + public static async Task EnsureTailwindCliAsync(string projectRoot) + { + var binPath = Path.Combine(projectRoot, ".shellui", "bin"); + Directory.CreateDirectory(binPath); + + var executableName = GetExecutableName(); + var tailwindPath = Path.Combine(binPath, executableName); + + if (File.Exists(tailwindPath)) + { + AnsiConsole.MarkupLine("[dim]Tailwind CLI already installed[/]"); + return tailwindPath; + } + + await DownloadTailwindCliAsync(tailwindPath); + return tailwindPath; + } + + private static async Task DownloadTailwindCliAsync(string destinationPath) + { + var platform = GetPlatformIdentifier(); + var downloadUrl = $"{BaseUrl}/{TailwindVersion}/tailwindcss-{platform}"; + + AnsiConsole.MarkupLine($"[cyan]Downloading Tailwind CLI {TailwindVersion}...[/]"); + + await AnsiConsole.Status() + .StartAsync("Downloading...", async ctx => + { + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromMinutes(5); + + var response = await client.GetAsync(downloadUrl); + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to download Tailwind CLI: {response.StatusCode}"); + } + + var bytes = await response.Content.ReadAsByteArrayAsync(); + await File.WriteAllBytesAsync(destinationPath, bytes); + + // Make executable on Unix systems + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var process = System.Diagnostics.Process.Start("chmod", $"+x {destinationPath}"); + process?.WaitForExit(); + } + }); + + AnsiConsole.MarkupLine("[green]Tailwind CLI downloaded successfully![/]"); + } + + private static string GetPlatformIdentifier() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "windows-x64.exe", + Architecture.Arm64 => "windows-arm64.exe", + _ => throw new PlatformNotSupportedException("Unsupported Windows architecture") + }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "linux-x64", + Architecture.Arm64 => "linux-arm64", + Architecture.Arm => "linux-armv7", + _ => throw new PlatformNotSupportedException("Unsupported Linux architecture") + }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "macos-x64", + Architecture.Arm64 => "macos-arm64", + _ => throw new PlatformNotSupportedException("Unsupported macOS architecture") + }; + } + + throw new PlatformNotSupportedException("Unsupported operating system"); + } + + private static string GetExecutableName() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "tailwindcss.exe" + : "tailwindcss"; + } + + public static string GetTailwindPath(string projectRoot) + { + var binPath = Path.Combine(projectRoot, ".shellui", "bin"); + var executableName = GetExecutableName(); + return Path.Combine(binPath, executableName); + } +} + From 6991c29aaeceb5c056ec2ddb849d16ad4d8644e8 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:17:40 +0200 Subject: [PATCH 18/45] feat: Create ThemeToggle component with theme switching functionality and asynchronous initialization --- .../Components/ThemeToggle.razor | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/ShellUI.Components/Components/ThemeToggle.razor diff --git a/src/ShellUI.Components/Components/ThemeToggle.razor b/src/ShellUI.Components/Components/ThemeToggle.razor new file mode 100644 index 0000000..efb4ef1 --- /dev/null +++ b/src/ShellUI.Components/Components/ThemeToggle.razor @@ -0,0 +1,40 @@ +@namespace ShellUI.Components +@using ShellUI.Components.Services +@inject IThemeService ThemeService + + + +@code { + private bool _isDark = true; + + protected override async Task OnInitializedAsync() + { + var theme = await ThemeService.GetThemeAsync(); + _isDark = theme == "dark"; + } + + private async Task ToggleTheme() + { + await ThemeService.ToggleThemeAsync(); + _isDark = !_isDark; + } +} + From 04859b3b26bb09d4940c0b68cf58ac13d8be6ace Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:17:55 +0200 Subject: [PATCH 19/45] feat: Implement ThemeService for managing theme state and asynchronous theme operations --- .../Services/ThemeService.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/ShellUI.Components/Services/ThemeService.cs diff --git a/src/ShellUI.Components/Services/ThemeService.cs b/src/ShellUI.Components/Services/ThemeService.cs new file mode 100644 index 0000000..9eb610c --- /dev/null +++ b/src/ShellUI.Components/Services/ThemeService.cs @@ -0,0 +1,73 @@ +using Microsoft.JSInterop; + +namespace ShellUI.Components.Services; + +public interface IThemeService +{ + Task GetThemeAsync(); + Task SetThemeAsync(string theme); + Task ToggleThemeAsync(); + Task InitializeThemeAsync(); +} + +public class ThemeService : IThemeService +{ + private readonly IJSRuntime _jsRuntime; + private string _currentTheme = "dark"; + + public ThemeService(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + public async Task GetThemeAsync() + { + try + { + var theme = await _jsRuntime.InvokeAsync("localStorage.getItem", "theme"); + _currentTheme = string.IsNullOrEmpty(theme) ? "dark" : theme; + return _currentTheme; + } + catch + { + return _currentTheme; + } + } + + public async Task SetThemeAsync(string theme) + { + _currentTheme = theme; + + try + { + await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "theme", theme); + + if (theme == "dark") + { + await _jsRuntime.InvokeVoidAsync("eval", "document.documentElement.classList.add('dark')"); + } + else + { + await _jsRuntime.InvokeVoidAsync("eval", "document.documentElement.classList.remove('dark')"); + } + } + catch + { + // JSRuntime not available (pre-render) + } + } + + public async Task ToggleThemeAsync() + { + var currentTheme = await GetThemeAsync(); + var newTheme = currentTheme == "dark" ? "light" : "dark"; + await SetThemeAsync(newTheme); + } + + public async Task InitializeThemeAsync() + { + var theme = await GetThemeAsync(); + await SetThemeAsync(theme); + } +} + From aa1a501d33d2a2055f4c760294a7ad80385a6c58 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 00:28:46 +0200 Subject: [PATCH 20/45] refactor: Change HandleClick method from async to synchronous for improved performance --- NET9/BlazorInteractiveServer/Components/Pages/Home.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor index e64c31f..812e1c6 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor @@ -74,7 +74,7 @@ private int clickCount = 0; private bool isLoading = false; - private async Task HandleClick(MouseEventArgs args) + private void HandleClick(MouseEventArgs args) { clickCount++; } From 666e2db3f749e3d84250ceb03f59ed41aff65c98 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 22:45:57 +0200 Subject: [PATCH 21/45] feat: Add new UI components including Alert, Badge, Card, Dialog, Input, Label, Textarea, and update ThemeToggle with size and variant support --- src/ShellUI.Components/Components/Alert.razor | 62 +++++++++ src/ShellUI.Components/Components/Badge.razor | 52 ++++++++ src/ShellUI.Components/Components/Card.razor | 42 ++++++ .../Components/Dialog.razor | 66 ++++++++++ src/ShellUI.Components/Components/Input.razor | 64 +++++++++ src/ShellUI.Components/Components/Label.razor | 25 ++++ .../Components/Textarea.razor | 61 +++++++++ .../Components/ThemeToggle.razor | 123 ++++++++++++++++-- src/ShellUI.Templates/ComponentRegistry.cs | 16 ++- src/ShellUI.Templates/CssTemplates.cs | 100 +++++++++++--- .../Templates/AlertTemplate.cs | 82 ++++++++++++ .../Templates/BadgeTemplate.cs | 71 ++++++++++ .../Templates/CardTemplate.cs | 61 +++++++++ .../Templates/DialogTemplate.cs | 85 ++++++++++++ .../Templates/InputTemplate.cs | 83 ++++++++++++ .../Templates/LabelTemplate.cs | 44 +++++++ .../Templates/TextareaTemplate.cs | 80 ++++++++++++ .../Templates/ThemeToggleTemplate.cs | 90 ++++++++++++- 18 files changed, 1172 insertions(+), 35 deletions(-) create mode 100644 src/ShellUI.Components/Components/Alert.razor create mode 100644 src/ShellUI.Components/Components/Badge.razor create mode 100644 src/ShellUI.Components/Components/Card.razor create mode 100644 src/ShellUI.Components/Components/Dialog.razor create mode 100644 src/ShellUI.Components/Components/Input.razor create mode 100644 src/ShellUI.Components/Components/Label.razor create mode 100644 src/ShellUI.Components/Components/Textarea.razor create mode 100644 src/ShellUI.Templates/Templates/AlertTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BadgeTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CardTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DialogTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/InputTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/LabelTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TextareaTemplate.cs diff --git a/src/ShellUI.Components/Components/Alert.razor b/src/ShellUI.Components/Components/Alert.razor new file mode 100644 index 0000000..2fee74e --- /dev/null +++ b/src/ShellUI.Components/Components/Alert.razor @@ -0,0 +1,62 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public string Variant { get; set; } = "default"; + [Parameter] public string Title { get; set; } = ""; + [Parameter] public RenderFragment? Icon { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "relative w-full rounded-lg border p-4 flex gap-3", + "[&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px]", + "[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground" + }; + + // Add variant-specific styling + switch (Variant.ToLower()) + { + case "destructive": + classes.Add("border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"); + break; + case "success": + classes.Add("border-green-500/50 text-green-700 dark:border-green-500 dark:text-green-400 [&>svg]:text-green-600"); + break; + case "warning": + classes.Add("border-yellow-500/50 text-yellow-700 dark:border-yellow-500 dark:text-yellow-400 [&>svg]:text-yellow-600"); + break; + case "info": + classes.Add("border-blue-500/50 text-blue-700 dark:border-blue-500 dark:text-blue-400 [&>svg]:text-blue-600"); + break; + default: + classes.Add("bg-background text-foreground"); + break; + } + + return string.Join(" ", classes); + } +} diff --git a/src/ShellUI.Components/Components/Badge.razor b/src/ShellUI.Components/Components/Badge.razor new file mode 100644 index 0000000..2b914d7 --- /dev/null +++ b/src/ShellUI.Components/Components/Badge.razor @@ -0,0 +1,52 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public string Variant { get; set; } = "default"; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold", + "transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + }; + + // Add variant-specific styling + switch (Variant.ToLower()) + { + case "secondary": + classes.Add("border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80"); + break; + case "destructive": + classes.Add("border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80"); + break; + case "outline": + classes.Add("text-foreground"); + break; + case "success": + classes.Add("border-transparent bg-green-500 text-white hover:bg-green-600"); + break; + case "warning": + classes.Add("border-transparent bg-yellow-500 text-white hover:bg-yellow-600"); + break; + case "info": + classes.Add("border-transparent bg-blue-500 text-white hover:bg-blue-600"); + break; + default: + classes.Add("border-transparent bg-primary text-primary-foreground hover:bg-primary/80"); + break; + } + + return string.Join(" ", classes); + } +} + diff --git a/src/ShellUI.Components/Components/Card.razor b/src/ShellUI.Components/Components/Card.razor new file mode 100644 index 0000000..a320cdd --- /dev/null +++ b/src/ShellUI.Components/Components/Card.razor @@ -0,0 +1,42 @@ +@namespace ShellUI.Components + +
+ @if (Header != null) + { +
+ @Header +
+ } + +
+ @ChildContent +
+ + @if (Footer != null) + { +
+ @Footer +
+ } +
+ +@code { + [Parameter] public RenderFragment? Header { get; set; } + [Parameter] public RenderFragment? Footer { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "rounded-lg border bg-card text-card-foreground shadow-sm" + }; + + return string.Join(" ", classes); + } +} + diff --git a/src/ShellUI.Components/Components/Dialog.razor b/src/ShellUI.Components/Components/Dialog.razor new file mode 100644 index 0000000..a615c44 --- /dev/null +++ b/src/ShellUI.Components/Components/Dialog.razor @@ -0,0 +1,66 @@ +@namespace ShellUI.Components + +@if (IsOpen) +{ +
+ +
+ + +
+ @if (Title != null || Description != null) + { +
+ @if (Title != null) + { +

+ @Title +

+ } + @if (Description != null) + { +

+ @Description +

+ } +
+ } + +
+ @ChildContent +
+ + @if (Footer != null) + { +
+ @Footer +
+ } + + + +
+
+} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public string? Title { get; set; } + [Parameter] public string? Description { get; set; } + [Parameter] public RenderFragment? Footer { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + + private async Task Close() + { + IsOpen = false; + await OnClose.InvokeAsync(); + } +} + diff --git a/src/ShellUI.Components/Components/Input.razor b/src/ShellUI.Components/Components/Input.razor new file mode 100644 index 0000000..1e0df39 --- /dev/null +++ b/src/ShellUI.Components/Components/Input.razor @@ -0,0 +1,64 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public string Type { get; set; } = "text"; + [Parameter] public string Value { get; set; } = ""; + [Parameter] public string Placeholder { get; set; } = ""; + [Parameter] public bool Disabled { get; set; } + [Parameter] public bool ReadOnly { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isFocused = false; + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm", + "ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium", + "placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2", + "focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed", + "disabled:opacity-50" + }; + + return string.Join(" ", classes); + } + + private async Task HandleInput(ChangeEventArgs e) + { + var newValue = e.Value?.ToString() ?? ""; + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + + private async Task HandleFocus(FocusEventArgs e) + { + _isFocused = true; + await OnFocus.InvokeAsync(e); + } + + private async Task HandleBlur(FocusEventArgs e) + { + _isFocused = false; + await OnBlur.InvokeAsync(e); + } +} + diff --git a/src/ShellUI.Components/Components/Label.razor b/src/ShellUI.Components/Components/Label.razor new file mode 100644 index 0000000..5af8190 --- /dev/null +++ b/src/ShellUI.Components/Components/Label.razor @@ -0,0 +1,25 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public string For { get; set; } = ""; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + }; + + return string.Join(" ", classes); + } +} + diff --git a/src/ShellUI.Components/Components/Textarea.razor b/src/ShellUI.Components/Components/Textarea.razor new file mode 100644 index 0000000..9cd7f96 --- /dev/null +++ b/src/ShellUI.Components/Components/Textarea.razor @@ -0,0 +1,61 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public string Value { get; set; } = ""; + [Parameter] public string Placeholder { get; set; } = ""; + [Parameter] public bool Disabled { get; set; } + [Parameter] public bool ReadOnly { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isFocused = false; + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm", + "ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none", + "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + "disabled:cursor-not-allowed disabled:opacity-50" + }; + + return string.Join(" ", classes); + } + + private async Task HandleInput(ChangeEventArgs e) + { + var newValue = e.Value?.ToString() ?? ""; + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + + private async Task HandleFocus(FocusEventArgs e) + { + _isFocused = true; + await OnFocus.InvokeAsync(e); + } + + private async Task HandleBlur(FocusEventArgs e) + { + _isFocused = false; + await OnBlur.InvokeAsync(e); + } +} + diff --git a/src/ShellUI.Components/Components/ThemeToggle.razor b/src/ShellUI.Components/Components/ThemeToggle.razor index efb4ef1..f04f047 100644 --- a/src/ShellUI.Components/Components/ThemeToggle.razor +++ b/src/ShellUI.Components/Components/ThemeToggle.razor @@ -1,40 +1,145 @@ @namespace ShellUI.Components -@using ShellUI.Components.Services -@inject IThemeService ThemeService +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime +@implements IAsyncDisposable @code { + private static readonly List _instances = new(); private bool _isDark = true; + [Parameter] + public string Size { get; set; } = "default"; + + [Parameter] + public string Variant { get; set; } = "ghost"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string IconSize => Size.ToLower() switch + { + "sm" => "h-4 w-4", + "lg" => "h-6 w-6", + _ => "h-5 w-5" + }; + + private string BuildCssClass() + { + var classes = new List + { + "inline-flex", + "items-center", + "justify-center", + "rounded-md", + "text-sm", + "font-medium", + "transition-colors", + "focus-visible:outline-none", + "focus-visible:ring-1", + "focus-visible:ring-ring", + "disabled:pointer-events-none", + "disabled:opacity-50" + }; + + // Variant styles + classes.Add(Variant.ToLower() switch + { + "ghost" => "hover:bg-accent hover:text-accent-foreground", + "outline" => "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + _ => "hover:bg-accent hover:text-accent-foreground" + }); + + // Size styles + classes.Add(Size.ToLower() switch + { + "sm" => "h-8 w-8", + "lg" => "h-11 w-11", + _ => "h-9 w-9" + }); + + return string.Join(" ", classes); + } + protected override async Task OnInitializedAsync() { - var theme = await ThemeService.GetThemeAsync(); - _isDark = theme == "dark"; + _instances.Add(this); + + try + { + var theme = await JSRuntime.InvokeAsync("localStorage.getItem", "theme"); + _isDark = string.IsNullOrEmpty(theme) ? true : theme == "dark"; + } + catch + { + _isDark = true; + } } private async Task ToggleTheme() { - await ThemeService.ToggleThemeAsync(); _isDark = !_isDark; + var theme = _isDark ? "dark" : "light"; + + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "theme", theme); + + if (_isDark) + { + await JSRuntime.InvokeVoidAsync("eval", "document.documentElement.classList.add('dark')"); + } + else + { + await JSRuntime.InvokeVoidAsync("eval", "document.documentElement.classList.remove('dark')"); + } + + // Update all ThemeToggle instances + foreach (var instance in _instances) + { + if (instance != this) + { + instance._isDark = _isDark; + instance.StateHasChanged(); + } + } + + StateHasChanged(); + } + catch + { + // JSRuntime not available (pre-render) + } + } + + public async ValueTask DisposeAsync() + { + _instances.Remove(this); + await ValueTask.CompletedTask; } } + diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index 77b3766..8b8cc79 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -8,7 +8,14 @@ public static class ComponentRegistry public static readonly Dictionary Components = new() { { "button", ButtonTemplate.Metadata }, - { "theme-toggle", ThemeToggleTemplate.Metadata } + { "theme-toggle", ThemeToggleTemplate.Metadata }, + { "input", InputTemplate.Metadata }, + { "card", CardTemplate.Metadata }, + { "alert", AlertTemplate.Metadata }, + { "badge", BadgeTemplate.Metadata }, + { "label", LabelTemplate.Metadata }, + { "textarea", TextareaTemplate.Metadata }, + { "dialog", DialogTemplate.Metadata } }; public static string? GetComponentContent(string componentName) @@ -17,6 +24,13 @@ public static class ComponentRegistry { "button" => ButtonTemplate.Content, "theme-toggle" => ThemeToggleTemplate.Content, + "input" => InputTemplate.Content, + "card" => CardTemplate.Content, + "alert" => AlertTemplate.Content, + "badge" => BadgeTemplate.Content, + "label" => LabelTemplate.Content, + "textarea" => TextareaTemplate.Content, + "dialog" => DialogTemplate.Content, _ => null }; } diff --git a/src/ShellUI.Templates/CssTemplates.cs b/src/ShellUI.Templates/CssTemplates.cs index d74a68e..ea44ef3 100644 --- a/src/ShellUI.Templates/CssTemplates.cs +++ b/src/ShellUI.Templates/CssTemplates.cs @@ -2,23 +2,86 @@ namespace ShellUI.Templates; public static class CssTemplates { - public static string InputCss => @"@tailwind base; -@tailwind components; -@tailwind utilities; + public static string InputCss => @"@import ""tailwindcss""; -/* - * ShellUI - Customize your theme here! - * - * Add your color variables and theme customizations below. - * These will be processed by the Tailwind standalone CLI. - * - * Example: - * - * @theme { - * --color-primary: #3b82f6; - * --color-secondary: #64748b; - * } - */ +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); +} + +:root { + --radius: 0.5rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.269 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.371 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} "; public static string AppCss => @"/* @@ -39,9 +102,6 @@ public static class CssTemplates './Pages/**/*.{razor,html,cshtml}', ], darkMode: 'class', - theme: { - extend: {}, - }, } "; -} \ No newline at end of file +} diff --git a/src/ShellUI.Templates/Templates/AlertTemplate.cs b/src/ShellUI.Templates/Templates/AlertTemplate.cs new file mode 100644 index 0000000..fa135d8 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertTemplate.cs @@ -0,0 +1,82 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class AlertTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert", + DisplayName = "Alert", + Description = "Notification and status message component", + Category = ComponentCategory.Feedback, + Version = "0.1.0", + FilePath = "Alert.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
+ @if (Icon != null) + { +
+ @Icon +
+ } +
+ @if (!string.IsNullOrEmpty(Title)) + { +
@Title
+ } +
+ @ChildContent +
+
+
+ +@code { + [Parameter] public string Variant { get; set; } = ""default""; + [Parameter] public string Title { get; set; } = """"; + [Parameter] public RenderFragment? Icon { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + ""relative w-full rounded-lg border p-4 flex gap-3"", + ""[&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px]"", + ""[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground"" + }; + + // Add variant-specific styling + switch (Variant.ToLower()) + { + case ""destructive"": + classes.Add(""border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive""); + break; + case ""success"": + classes.Add(""border-green-500/50 text-green-700 dark:border-green-500 dark:text-green-400 [&>svg]:text-green-600""); + break; + case ""warning"": + classes.Add(""border-yellow-500/50 text-yellow-700 dark:border-yellow-500 dark:text-yellow-400 [&>svg]:text-yellow-600""); + break; + case ""info"": + classes.Add(""border-blue-500/50 text-blue-700 dark:border-blue-500 dark:text-blue-400 [&>svg]:text-blue-600""); + break; + default: + classes.Add(""bg-background text-foreground""); + break; + } + + return string.Join("" "", classes); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/BadgeTemplate.cs b/src/ShellUI.Templates/Templates/BadgeTemplate.cs new file mode 100644 index 0000000..505239d --- /dev/null +++ b/src/ShellUI.Templates/Templates/BadgeTemplate.cs @@ -0,0 +1,71 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class BadgeTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "badge", + DisplayName = "Badge", + Description = "Small status indicator component", + Category = ComponentCategory.DataDisplay, + Version = "0.1.0", + FilePath = "Badge.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
+ @ChildContent +
+ +@code { + [Parameter] public string Variant { get; set; } = ""default""; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + ""inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold"", + ""transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"" + }; + + // Add variant-specific styling + switch (Variant.ToLower()) + { + case ""secondary"": + classes.Add(""border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80""); + break; + case ""destructive"": + classes.Add(""border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80""); + break; + case ""outline"": + classes.Add(""text-foreground""); + break; + case ""success"": + classes.Add(""border-transparent bg-green-500 text-white hover:bg-green-600""); + break; + case ""warning"": + classes.Add(""border-transparent bg-yellow-500 text-white hover:bg-yellow-600""); + break; + case ""info"": + classes.Add(""border-transparent bg-blue-500 text-white hover:bg-blue-600""); + break; + default: + classes.Add(""border-transparent bg-primary text-primary-foreground hover:bg-primary/80""); + break; + } + + return string.Join("" "", classes); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/CardTemplate.cs b/src/ShellUI.Templates/Templates/CardTemplate.cs new file mode 100644 index 0000000..e96eb84 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CardTemplate.cs @@ -0,0 +1,61 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class CardTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "card", + DisplayName = "Card", + Description = "Container component for grouping related content", + Category = ComponentCategory.Layout, + Version = "0.1.0", + FilePath = "Card.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
+ @if (Header != null) + { +
+ @Header +
+ } + +
+ @ChildContent +
+ + @if (Footer != null) + { +
+ @Footer +
+ } +
+ +@code { + [Parameter] public RenderFragment? Header { get; set; } + [Parameter] public RenderFragment? Footer { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + ""rounded-lg border bg-card text-card-foreground shadow-sm"" + }; + + return string.Join("" "", classes); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/DialogTemplate.cs b/src/ShellUI.Templates/Templates/DialogTemplate.cs new file mode 100644 index 0000000..b99daa7 --- /dev/null +++ b/src/ShellUI.Templates/Templates/DialogTemplate.cs @@ -0,0 +1,85 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class DialogTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "dialog", + DisplayName = "Dialog", + Description = "Modal dialog component", + Category = ComponentCategory.Overlay, + Version = "0.1.0", + FilePath = "Dialog.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@if (IsOpen) +{ +
+ +
+ + +
+ @if (Title != null || Description != null) + { +
+ @if (Title != null) + { +

+ @Title +

+ } + @if (Description != null) + { +

+ @Description +

+ } +
+ } + +
+ @ChildContent +
+ + @if (Footer != null) + { +
+ @Footer +
+ } + + + +
+
+} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public string? Title { get; set; } + [Parameter] public string? Description { get; set; } + [Parameter] public RenderFragment? Footer { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + + private async Task Close() + { + IsOpen = false; + await OnClose.InvokeAsync(); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/InputTemplate.cs b/src/ShellUI.Templates/Templates/InputTemplate.cs new file mode 100644 index 0000000..5685375 --- /dev/null +++ b/src/ShellUI.Templates/Templates/InputTemplate.cs @@ -0,0 +1,83 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class InputTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "input", + DisplayName = "Input", + Description = "Accessible text input component with focus states", + Category = ComponentCategory.Form, + Version = "0.1.0", + FilePath = "Input.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] public string Type { get; set; } = ""text""; + [Parameter] public string Value { get; set; } = """"; + [Parameter] public string Placeholder { get; set; } = """"; + [Parameter] public bool Disabled { get; set; } + [Parameter] public bool ReadOnly { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isFocused = false; + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + ""flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"", + ""ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium"", + ""placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2"", + ""focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed"", + ""disabled:opacity-50"" + }; + + return string.Join("" "", classes); + } + + private async Task HandleInput(ChangeEventArgs e) + { + var newValue = e.Value?.ToString() ?? """"; + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + + private async Task HandleFocus(FocusEventArgs e) + { + _isFocused = true; + await OnFocus.InvokeAsync(e); + } + + private async Task HandleBlur(FocusEventArgs e) + { + _isFocused = false; + await OnBlur.InvokeAsync(e); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/LabelTemplate.cs b/src/ShellUI.Templates/Templates/LabelTemplate.cs new file mode 100644 index 0000000..8ef4958 --- /dev/null +++ b/src/ShellUI.Templates/Templates/LabelTemplate.cs @@ -0,0 +1,44 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class LabelTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "label", + DisplayName = "Label", + Description = "Form label component", + Category = ComponentCategory.Form, + Version = "0.1.0", + FilePath = "Label.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] public string For { get; set; } = """"; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + ""text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"" + }; + + return string.Join("" "", classes); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TextareaTemplate.cs b/src/ShellUI.Templates/Templates/TextareaTemplate.cs new file mode 100644 index 0000000..8bba024 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TextareaTemplate.cs @@ -0,0 +1,80 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class TextareaTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "textarea", + DisplayName = "Textarea", + Description = "Multi-line text input component", + Category = ComponentCategory.Form, + Version = "0.1.0", + FilePath = "Textarea.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] public string Value { get; set; } = """"; + [Parameter] public string Placeholder { get; set; } = """"; + [Parameter] public bool Disabled { get; set; } + [Parameter] public bool ReadOnly { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isFocused = false; + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + ""flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm"", + ""ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none"", + ""focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"", + ""disabled:cursor-not-allowed disabled:opacity-50"" + }; + + return string.Join("" "", classes); + } + + private async Task HandleInput(ChangeEventArgs e) + { + var newValue = e.Value?.ToString() ?? """"; + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + + private async Task HandleFocus(FocusEventArgs e) + { + _isFocused = true; + await OnFocus.InvokeAsync(e); + } + + private async Task HandleBlur(FocusEventArgs e) + { + _isFocused = false; + await OnBlur.InvokeAsync(e); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs index cf782cc..0823916 100644 --- a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs +++ b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs @@ -15,34 +15,96 @@ public static class ThemeToggleTemplate Dependencies = new List() }; - public static string Content => @"@using Microsoft.JSInterop + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using Microsoft.JSInterop @inject IJSRuntime JSRuntime +@implements IAsyncDisposable @code { + private static readonly List _instances = new(); private bool _isDark = true; + [Parameter] + public string Size { get; set; } = ""default""; + + [Parameter] + public string Variant { get; set; } = ""ghost""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string IconSize => Size.ToLower() switch + { + ""sm"" => ""h-4 w-4"", + ""lg"" => ""h-6 w-6"", + _ => ""h-5 w-5"" + }; + + private string BuildCssClass() + { + var classes = new List + { + ""inline-flex"", + ""items-center"", + ""justify-center"", + ""rounded-md"", + ""text-sm"", + ""font-medium"", + ""transition-colors"", + ""focus-visible:outline-none"", + ""focus-visible:ring-1"", + ""focus-visible:ring-ring"", + ""disabled:pointer-events-none"", + ""disabled:opacity-50"" + }; + + // Variant styles + classes.Add(Variant.ToLower() switch + { + ""ghost"" => ""hover:bg-accent hover:text-accent-foreground"", + ""outline"" => ""border border-input bg-background hover:bg-accent hover:text-accent-foreground"", + _ => ""hover:bg-accent hover:text-accent-foreground"" + }); + + // Size styles + classes.Add(Size.ToLower() switch + { + ""sm"" => ""h-8 w-8"", + ""lg"" => ""h-11 w-11"", + _ => ""h-9 w-9"" + }); + + return string.Join("" "", classes); + } + protected override async Task OnInitializedAsync() { + _instances.Add(this); + try { var theme = await JSRuntime.InvokeAsync(""localStorage.getItem"", ""theme""); @@ -71,12 +133,30 @@ private async Task ToggleTheme() { await JSRuntime.InvokeVoidAsync(""eval"", ""document.documentElement.classList.remove('dark')""); } + + // Update all ThemeToggle instances + foreach (var instance in _instances) + { + if (instance != this) + { + instance._isDark = _isDark; + instance.StateHasChanged(); + } + } + + StateHasChanged(); } catch { // JSRuntime not available (pre-render) } } + + public async ValueTask DisposeAsync() + { + _instances.Remove(this); + await ValueTask.CompletedTask; + } } "; From e73e58e0f3791e73414ac728fe8a259288bd126d Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 22:48:06 +0200 Subject: [PATCH 22/45] feat: Introduce new UI components including Alert, Badge, Card, Dialog, Input, Label, and Textarea, enhancing the component library for better user interface design --- .../Components/UI/Alert.razor | 62 +++++++++++++ .../Components/UI/Badge.razor | 51 +++++++++++ .../Components/UI/Card.razor | 41 +++++++++ .../Components/UI/Dialog.razor | 65 ++++++++++++++ .../Components/UI/Input.razor | 63 ++++++++++++++ .../Components/UI/Label.razor | 24 +++++ .../Components/UI/Textarea.razor | 60 +++++++++++++ .../Components/UI/ThemeToggle.razor | 87 ++++++++++++++++++- 8 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Alert.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Badge.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Card.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Dialog.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Input.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Label.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Textarea.razor diff --git a/NET9/BlazorInteractiveServer/Components/UI/Alert.razor b/NET9/BlazorInteractiveServer/Components/UI/Alert.razor new file mode 100644 index 0000000..24ea112 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Alert.razor @@ -0,0 +1,62 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] public string Variant { get; set; } = "default"; + [Parameter] public string Title { get; set; } = ""; + [Parameter] public RenderFragment? Icon { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "relative w-full rounded-lg border p-4 flex gap-3", + "[&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px]", + "[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground" + }; + + // Add variant-specific styling + switch (Variant.ToLower()) + { + case "destructive": + classes.Add("border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"); + break; + case "success": + classes.Add("border-green-500/50 text-green-700 dark:border-green-500 dark:text-green-400 [&>svg]:text-green-600"); + break; + case "warning": + classes.Add("border-yellow-500/50 text-yellow-700 dark:border-yellow-500 dark:text-yellow-400 [&>svg]:text-yellow-600"); + break; + case "info": + classes.Add("border-blue-500/50 text-blue-700 dark:border-blue-500 dark:text-blue-400 [&>svg]:text-blue-600"); + break; + default: + classes.Add("bg-background text-foreground"); + break; + } + + return string.Join(" ", classes); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Badge.razor b/NET9/BlazorInteractiveServer/Components/UI/Badge.razor new file mode 100644 index 0000000..ef10ed6 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Badge.razor @@ -0,0 +1,51 @@ +@namespace BlazorInteractiveServer.Components.UI + +
+ @ChildContent +
+ +@code { + [Parameter] public string Variant { get; set; } = "default"; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold", + "transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + }; + + // Add variant-specific styling + switch (Variant.ToLower()) + { + case "secondary": + classes.Add("border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80"); + break; + case "destructive": + classes.Add("border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80"); + break; + case "outline": + classes.Add("text-foreground"); + break; + case "success": + classes.Add("border-transparent bg-green-500 text-white hover:bg-green-600"); + break; + case "warning": + classes.Add("border-transparent bg-yellow-500 text-white hover:bg-yellow-600"); + break; + case "info": + classes.Add("border-transparent bg-blue-500 text-white hover:bg-blue-600"); + break; + default: + classes.Add("border-transparent bg-primary text-primary-foreground hover:bg-primary/80"); + break; + } + + return string.Join(" ", classes); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Card.razor b/NET9/BlazorInteractiveServer/Components/UI/Card.razor new file mode 100644 index 0000000..af738e3 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Card.razor @@ -0,0 +1,41 @@ +@namespace BlazorInteractiveServer.Components.UI + +
+ @if (Header != null) + { +
+ @Header +
+ } + +
+ @ChildContent +
+ + @if (Footer != null) + { +
+ @Footer +
+ } +
+ +@code { + [Parameter] public RenderFragment? Header { get; set; } + [Parameter] public RenderFragment? Footer { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "rounded-lg border bg-card text-card-foreground shadow-sm" + }; + + return string.Join(" ", classes); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Dialog.razor b/NET9/BlazorInteractiveServer/Components/UI/Dialog.razor new file mode 100644 index 0000000..ad5a378 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Dialog.razor @@ -0,0 +1,65 @@ +@namespace BlazorInteractiveServer.Components.UI + +@if (IsOpen) +{ +
+ +
+ + +
+ @if (Title != null || Description != null) + { +
+ @if (Title != null) + { +

+ @Title +

+ } + @if (Description != null) + { +

+ @Description +

+ } +
+ } + +
+ @ChildContent +
+ + @if (Footer != null) + { +
+ @Footer +
+ } + + + +
+
+} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public string? Title { get; set; } + [Parameter] public string? Description { get; set; } + [Parameter] public RenderFragment? Footer { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + + private async Task Close() + { + IsOpen = false; + await OnClose.InvokeAsync(); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Input.razor b/NET9/BlazorInteractiveServer/Components/UI/Input.razor new file mode 100644 index 0000000..78779e8 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Input.razor @@ -0,0 +1,63 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] public string Type { get; set; } = "text"; + [Parameter] public string Value { get; set; } = ""; + [Parameter] public string Placeholder { get; set; } = ""; + [Parameter] public bool Disabled { get; set; } + [Parameter] public bool ReadOnly { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isFocused = false; + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm", + "ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium", + "placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2", + "focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed", + "disabled:opacity-50" + }; + + return string.Join(" ", classes); + } + + private async Task HandleInput(ChangeEventArgs e) + { + var newValue = e.Value?.ToString() ?? ""; + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + + private async Task HandleFocus(FocusEventArgs e) + { + _isFocused = true; + await OnFocus.InvokeAsync(e); + } + + private async Task HandleBlur(FocusEventArgs e) + { + _isFocused = false; + await OnBlur.InvokeAsync(e); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Label.razor b/NET9/BlazorInteractiveServer/Components/UI/Label.razor new file mode 100644 index 0000000..3e827e2 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Label.razor @@ -0,0 +1,24 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] public string For { get; set; } = ""; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + }; + + return string.Join(" ", classes); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Textarea.razor b/NET9/BlazorInteractiveServer/Components/UI/Textarea.razor new file mode 100644 index 0000000..8cfa9fa --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Textarea.razor @@ -0,0 +1,60 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] public string Value { get; set; } = ""; + [Parameter] public string Placeholder { get; set; } = ""; + [Parameter] public bool Disabled { get; set; } + [Parameter] public bool ReadOnly { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isFocused = false; + + private string CssClass => BuildCssClass(); + + private string BuildCssClass() + { + var classes = new List + { + "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm", + "ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none", + "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + "disabled:cursor-not-allowed disabled:opacity-50" + }; + + return string.Join(" ", classes); + } + + private async Task HandleInput(ChangeEventArgs e) + { + var newValue = e.Value?.ToString() ?? ""; + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + + private async Task HandleFocus(FocusEventArgs e) + { + _isFocused = true; + await OnFocus.InvokeAsync(e); + } + + private async Task HandleBlur(FocusEventArgs e) + { + _isFocused = false; + await OnBlur.InvokeAsync(e); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor index 7a35f4c..e93b143 100644 --- a/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor +++ b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor @@ -1,31 +1,92 @@ @using Microsoft.JSInterop @inject IJSRuntime JSRuntime +@implements IAsyncDisposable @code { + private static readonly List _instances = new(); private bool _isDark = true; + [Parameter] + public string Size { get; set; } = "default"; + + [Parameter] + public string Variant { get; set; } = "ghost"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string CssClass => BuildCssClass(); + + private string IconSize => Size.ToLower() switch + { + "sm" => "h-4 w-4", + "lg" => "h-6 w-6", + _ => "h-5 w-5" + }; + + private string BuildCssClass() + { + var classes = new List + { + "inline-flex", + "items-center", + "justify-center", + "rounded-md", + "text-sm", + "font-medium", + "transition-colors", + "focus-visible:outline-none", + "focus-visible:ring-1", + "focus-visible:ring-ring", + "disabled:pointer-events-none", + "disabled:opacity-50" + }; + + // Variant styles + classes.Add(Variant.ToLower() switch + { + "ghost" => "hover:bg-accent hover:text-accent-foreground", + "outline" => "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + _ => "hover:bg-accent hover:text-accent-foreground" + }); + + // Size styles + classes.Add(Size.ToLower() switch + { + "sm" => "h-8 w-8", + "lg" => "h-11 w-11", + _ => "h-9 w-9" + }); + + return string.Join(" ", classes); + } + protected override async Task OnInitializedAsync() { + _instances.Add(this); + try { var theme = await JSRuntime.InvokeAsync("localStorage.getItem", "theme"); @@ -54,10 +115,28 @@ { await JSRuntime.InvokeVoidAsync("eval", "document.documentElement.classList.remove('dark')"); } + + // Update all ThemeToggle instances + foreach (var instance in _instances) + { + if (instance != this) + { + instance._isDark = _isDark; + instance.StateHasChanged(); + } + } + + StateHasChanged(); } catch { // JSRuntime not available (pre-render) } } + + public async ValueTask DisposeAsync() + { + _instances.Remove(this); + await ValueTask.CompletedTask; + } } From 00ebb4a2f4048c153155dcd9e096c383077306c7 Mon Sep 17 00:00:00 2001 From: Shewart Date: Tue, 14 Oct 2025 22:48:50 +0200 Subject: [PATCH 23/45] refactor: Update Tailwind CSS configuration and input styles to enhance theme customization and improve utility classes --- .../tailwind.config.js | 3 - NET9/BlazorInteractiveServer/wwwroot/app.css | 1276 +++++++++++++++-- .../BlazorInteractiveServer/wwwroot/input.css | 95 +- 3 files changed, 1256 insertions(+), 118 deletions(-) diff --git a/NET9/BlazorInteractiveServer/tailwind.config.js b/NET9/BlazorInteractiveServer/tailwind.config.js index b1ac5ee..e3e016d 100644 --- a/NET9/BlazorInteractiveServer/tailwind.config.js +++ b/NET9/BlazorInteractiveServer/tailwind.config.js @@ -5,7 +5,4 @@ export default { './Pages/**/*.{razor,html,cshtml}', ], darkMode: 'class', - theme: { - extend: {}, - }, } diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index 2ebef71..d9b02a7 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -2,8 +2,11 @@ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @layer base { *, ::before, ::after, ::backdrop { + --tw-space-y-reverse: 0; --tw-border-style: solid; - --tw-outline-style: solid; + --tw-leading: initial; + --tw-font-weight: initial; + --tw-tracking: initial; --tw-shadow: 0 0 #0000; --tw-shadow-color: initial; --tw-shadow-alpha: 100%; @@ -18,128 +21,1084 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + --tw-space-x-reverse: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; } } } -.static { - position: static; -} -.container { - width: 100%; -} -.mx-auto { - margin-inline: auto; -} -.flex { - display: flex; -} -.inline-flex { - display: inline-flex; -} -.min-h-screen { - min-height: 100vh; -} -.flex-wrap { - flex-wrap: wrap; -} -.items-center { - align-items: center; -} -.justify-between { - justify-content: space-between; -} -.justify-center { - justify-content: center; -} -.justify-end { - justify-content: flex-end; -} -.border { - border-style: var(--tw-border-style); - border-width: 1px; -} -.border-t { - border-top-style: var(--tw-border-style); - border-top-width: 1px; -} -.border-b { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 1px; -} -.text-center { - text-align: center; -} -.whitespace-nowrap { - white-space: nowrap; -} -.underline-offset-4 { - text-underline-offset: 4px; -} -.opacity-25 { - opacity: 25%; -} -.opacity-75 { - opacity: 75%; -} -.outline { - outline-style: var(--tw-outline-style); - outline-width: 1px; -} -.transition-colors { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var(--tw-ease, ease); - transition-duration: var(--tw-duration, 0s); +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + --color-yellow-400: oklch(85.2% 0.199 91.936); + --color-yellow-500: oklch(79.5% 0.184 86.047); + --color-yellow-600: oklch(68.1% 0.162 75.834); + --color-yellow-700: oklch(55.4% 0.135 66.442); + --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-600: oklch(62.7% 0.194 149.214); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-blue-400: oklch(70.7% 0.165 254.624); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-700: oklch(48.8% 0.243 264.376); + --color-black: #000; + --color-white: #fff; + --spacing: 0.25rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-4xl: 56rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --tracking-tight: -0.025em; + --animate-spin: spin 1s linear infinite; + --blur-sm: 8px; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } } -.hover\:underline { - &:hover { - @media (hover: hover) { - text-decoration-line: underline; +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: color-mix(in oklab, currentColor 50%, transparent); } } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; + } } -.focus-visible\:ring-1 { - &:focus-visible { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); +@layer utilities { + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .top-4 { + top: calc(var(--spacing) * 4); + } + .right-4 { + right: calc(var(--spacing) * 4); + } + .z-50 { + z-index: 50; + } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } + .mx-auto { + margin-inline: auto; + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mt-6 { + margin-top: calc(var(--spacing) * 6); + } + .mt-12 { + margin-top: calc(var(--spacing) * 12); + } + .mr-3 { + margin-right: calc(var(--spacing) * 3); + } + .mb-1 { + margin-bottom: calc(var(--spacing) * 1); + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .-ml-1 { + margin-left: calc(var(--spacing) * -1); + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .inline-flex { + display: inline-flex; + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-6 { + height: calc(var(--spacing) * 6); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-9 { + height: calc(var(--spacing) * 9); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-11 { + height: calc(var(--spacing) * 11); + } + .min-h-\[80px\] { + min-height: 80px; + } + .min-h-screen { + min-height: 100vh; + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-6 { + width: calc(var(--spacing) * 6); + } + .w-8 { + width: calc(var(--spacing) * 8); + } + .w-9 { + width: calc(var(--spacing) * 9); + } + .w-10 { + width: calc(var(--spacing) * 10); + } + .w-11 { + width: calc(var(--spacing) * 11); + } + .w-full { + width: 100%; + } + .max-w-4xl { + max-width: var(--container-4xl); + } + .max-w-lg { + max-width: var(--container-lg); + } + .max-w-md { + max-width: var(--container-md); + } + .flex-1 { + flex: 1; + } + .flex-shrink-0 { + flex-shrink: 0; + } + .animate-spin { + animation: var(--animate-spin); + } + .flex-col { + flex-direction: column; + } + .flex-col-reverse { + flex-direction: column-reverse; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-3 { + gap: calc(var(--spacing) * 3); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .gap-6 { + gap: calc(var(--spacing) * 6); + } + .space-y-1\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-4 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-8 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); + } + } + .rounded { + border-radius: 0.25rem; + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius); + } + .rounded-md { + border-radius: calc(var(--radius) - 2px); + } + .rounded-sm { + border-radius: calc(var(--radius) - 4px); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-blue-500\/50 { + border-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-blue-500) 50%, transparent); + } + } + .border-border { + border-color: var(--border); + } + .border-destructive\/50 { + border-color: color-mix(in oklab, var(--destructive) 50%, transparent); + } + .border-green-500\/50 { + border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-green-500) 50%, transparent); + } + } + .border-input { + border-color: var(--input); + } + .border-transparent { + border-color: transparent; + } + .border-yellow-500\/50 { + border-color: color-mix(in srgb, oklch(79.5% 0.184 86.047) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-yellow-500) 50%, transparent); + } + } + .bg-accent { + background-color: var(--accent); + } + .bg-background { + background-color: var(--background); + } + .bg-black\/50 { + background-color: color-mix(in srgb, #000 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 50%, transparent); + } + } + .bg-blue-500 { + background-color: var(--color-blue-500); + } + .bg-card { + background-color: var(--card); + } + .bg-destructive { + background-color: var(--destructive); + } + .bg-green-500 { + background-color: var(--color-green-500); + } + .bg-muted { + background-color: var(--muted); + } + .bg-primary { + background-color: var(--primary); + } + .bg-secondary { + background-color: var(--secondary); + } + .bg-yellow-500 { + background-color: var(--color-yellow-500); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-2\.5 { + padding-inline: calc(var(--spacing) * 2.5); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-8 { + padding-inline: calc(var(--spacing) * 8); + } + .py-0\.5 { + padding-block: calc(var(--spacing) * 0.5); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + .py-6 { + padding-block: calc(var(--spacing) * 6); + } + .pt-0 { + padding-top: calc(var(--spacing) * 0); + } + .text-center { + text-align: center; + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .leading-none { + --tw-leading: 1; + line-height: 1; + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .tracking-tight { + --tw-tracking: var(--tracking-tight); + letter-spacing: var(--tracking-tight); + } + .whitespace-nowrap { + white-space: nowrap; + } + .text-accent-foreground { + color: var(--accent-foreground); + } + .text-blue-700 { + color: var(--color-blue-700); + } + .text-card-foreground { + color: var(--card-foreground); + } + .text-destructive { + color: var(--destructive); + } + .text-destructive-foreground { + color: var(--destructive-foreground); + } + .text-foreground { + color: var(--foreground); + } + .text-green-700 { + color: var(--color-green-700); + } + .text-muted-foreground { + color: var(--muted-foreground); + } + .text-primary { + color: var(--primary); + } + .text-primary-foreground { + color: var(--primary-foreground); + } + .text-secondary-foreground { + color: var(--secondary-foreground); + } + .text-white { + color: var(--color-white); + } + .text-yellow-700 { + color: var(--color-yellow-700); + } + .underline-offset-4 { + text-underline-offset: 4px; + } + .opacity-25 { + opacity: 25%; + } + .opacity-70 { + opacity: 70%; + } + .opacity-75 { + opacity: 75%; + } + .opacity-90 { + opacity: 90%; + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } -} -.focus-visible\:ring-2 { - &:focus-visible { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } -} -.focus-visible\:ring-offset-2 { - &:focus-visible { - --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); + .ring-offset-background { + --tw-ring-offset-color: var(--background); } -} -.focus-visible\:outline-none { - &:focus-visible { - --tw-outline-style: none; - outline-style: none; + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; } -} -.disabled\:pointer-events-none { - &:disabled { - pointer-events: none; + .blur { + --tw-blur: blur(8px); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .backdrop-blur-sm { + --tw-backdrop-blur: blur(var(--blur-sm)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .transition-colors { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-opacity { + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .peer-disabled\:cursor-not-allowed { + &:is(:where(.peer):disabled ~ *) { + cursor: not-allowed; + } } + .peer-disabled\:opacity-70 { + &:is(:where(.peer):disabled ~ *) { + opacity: 70%; + } + } + .file\:border-0 { + &::file-selector-button { + border-style: var(--tw-border-style); + border-width: 0px; + } + } + .file\:bg-transparent { + &::file-selector-button { + background-color: transparent; + } + } + .file\:text-sm { + &::file-selector-button { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + } + .file\:font-medium { + &::file-selector-button { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + } + .placeholder\:text-muted-foreground { + &::placeholder { + color: var(--muted-foreground); + } + } + .hover\:bg-accent { + &:hover { + @media (hover: hover) { + background-color: var(--accent); + } + } + } + .hover\:bg-blue-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-600); + } + } + } + .hover\:bg-destructive\/80 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--destructive) 80%, transparent); + } + } + } + .hover\:bg-destructive\/90 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--destructive) 90%, transparent); + } + } + } + .hover\:bg-green-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-green-600); + } + } + } + .hover\:bg-primary\/80 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--primary) 80%, transparent); + } + } + } + .hover\:bg-primary\/90 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--primary) 90%, transparent); + } + } + } + .hover\:bg-secondary\/80 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--secondary) 80%, transparent); + } + } + } + .hover\:bg-yellow-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-yellow-600); + } + } + } + .hover\:text-accent-foreground { + &:hover { + @media (hover: hover) { + color: var(--accent-foreground); + } + } + } + .hover\:text-foreground { + &:hover { + @media (hover: hover) { + color: var(--foreground); + } + } + } + .hover\:underline { + &:hover { + @media (hover: hover) { + text-decoration-line: underline; + } + } + } + .hover\:opacity-100 { + &:hover { + @media (hover: hover) { + opacity: 100%; + } + } + } + .focus\:ring-2 { + &:focus { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus\:ring-ring { + &:focus { + --tw-ring-color: var(--ring); + } + } + .focus\:ring-offset-2 { + &:focus { + --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); + } + } + .focus\:outline-none { + &:focus { + --tw-outline-style: none; + outline-style: none; + } + } + .focus-visible\:ring-1 { + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus-visible\:ring-2 { + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus-visible\:ring-ring { + &:focus-visible { + --tw-ring-color: var(--ring); + } + } + .focus-visible\:ring-offset-2 { + &:focus-visible { + --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); + } + } + .focus-visible\:outline-none { + &:focus-visible { + --tw-outline-style: none; + outline-style: none; + } + } + .disabled\:pointer-events-none { + &:disabled { + pointer-events: none; + } + } + .disabled\:cursor-not-allowed { + &:disabled { + cursor: not-allowed; + } + } + .disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } + } + .sm\:flex-row { + @media (width >= 40rem) { + flex-direction: row; + } + } + .sm\:justify-end { + @media (width >= 40rem) { + justify-content: flex-end; + } + } + .sm\:space-x-2 { + @media (width >= 40rem) { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))); + } + } + } + .sm\:text-left { + @media (width >= 40rem) { + text-align: left; + } + } + .md\:grid-cols-2 { + @media (width >= 48rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .dark\:border-blue-500 { + @media (prefers-color-scheme: dark) { + border-color: var(--color-blue-500); + } + } + .dark\:border-destructive { + @media (prefers-color-scheme: dark) { + border-color: var(--destructive); + } + } + .dark\:border-green-500 { + @media (prefers-color-scheme: dark) { + border-color: var(--color-green-500); + } + } + .dark\:border-yellow-500 { + @media (prefers-color-scheme: dark) { + border-color: var(--color-yellow-500); + } + } + .dark\:text-blue-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-blue-400); + } + } + .dark\:text-green-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-green-400); + } + } + .dark\:text-yellow-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-yellow-400); + } + } + .\[\&\>svg\]\:absolute { + &>svg { + position: absolute; + } + } + .\[\&\>svg\]\:top-4 { + &>svg { + top: calc(var(--spacing) * 4); + } + } + .\[\&\>svg\]\:left-4 { + &>svg { + left: calc(var(--spacing) * 4); + } + } + .\[\&\>svg\]\:text-blue-600 { + &>svg { + color: var(--color-blue-600); + } + } + .\[\&\>svg\]\:text-destructive { + &>svg { + color: var(--destructive); + } + } + .\[\&\>svg\]\:text-foreground { + &>svg { + color: var(--foreground); + } + } + .\[\&\>svg\]\:text-green-600 { + &>svg { + color: var(--color-green-600); + } + } + .\[\&\>svg\]\:text-yellow-600 { + &>svg { + color: var(--color-yellow-600); + } + } + .\[\&\>svg\+div\]\:translate-y-\[-3px\] { + &>svg+div { + --tw-translate-y: -3px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + .\[\&\>svg\~\*\]\:pl-7 { + &>svg~* { + padding-left: calc(var(--spacing) * 7); + } + } +} +:root { + --radius: 0.5rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); } -.disabled\:opacity-50 { - &:disabled { - opacity: 50%; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.269 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.371 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); +} +@layer base { + * { + border-color: var(--border); + } + body { + background-color: var(--background); + color: var(--foreground); } } +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-border-style { syntax: "*"; inherits: false; initial-value: solid; } -@property --tw-outline-style { +@property --tw-leading { + syntax: "*"; + inherits: false; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { syntax: "*"; inherits: false; - initial-value: solid; } @property --tw-shadow { syntax: "*"; @@ -206,3 +1165,122 @@ inherits: false; initial-value: 0 0 #0000; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/NET9/BlazorInteractiveServer/wwwroot/input.css b/NET9/BlazorInteractiveServer/wwwroot/input.css index a9e95b3..688a22a 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/input.css +++ b/NET9/BlazorInteractiveServer/wwwroot/input.css @@ -1,17 +1,80 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; -/* - * ShellUI - Customize your theme here! - * - * Add your color variables and theme customizations below. - * These will be processed by the Tailwind standalone CLI. - * - * Example: - * - * @theme { - * --color-primary: #3b82f6; - * --color-secondary: #64748b; - * } - */ +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); +} + +:root { + --radius: 0.5rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.269 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.371 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} From 701411777303c81dc28d9a69379c8a0006a69da1 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 15 Oct 2025 22:42:01 +0200 Subject: [PATCH 24/45] refactor: Replace dark mode toggle button with ThemeToggle component for improved theme management in MainLayout --- .../Components/Layout/MainLayout.razor | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index 3c9d7ef..6b331f8 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -1,5 +1,4 @@ @inherits LayoutComponentBase -@inject IJSRuntime JSRuntime
@@ -11,17 +10,8 @@ Docs GitHub - - + +
From 404048193cd3177bbcbbd875e1ecbadf1a31cee1 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 15 Oct 2025 22:43:03 +0200 Subject: [PATCH 25/45] feat: Revamp Home page layout and introduce new UI components including Form, Card, and Alert sections for enhanced user interaction and design consistency --- .../Components/Pages/Home.razor | 120 ++++++++++++++++-- .../Components/UI/Button.razor | 56 +------- .../Components/UI/ThemeToggle.razor | 54 +------- 3 files changed, 114 insertions(+), 116 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor index 812e1c6..44d77c5 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor @@ -1,16 +1,22 @@ @page "/" -ShellUI - Button Demo +ShellUI - Component Demo -
- -
- +
+
+

ShellUI Components

+

Beautiful, accessible components inspired by shadcn/ui

-
-

ShellUI Button Component

-

Beautiful, accessible buttons inspired by shadcn/ui

+ +
+

Responsive Design

+

All components support Tailwind's responsive utilities. Resize your browser to see it in action!

+
+ + + +
@@ -50,6 +56,73 @@
+ +
+

Form Components

+
+
+ + +
+
+ + +
+
+ + + class=""flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"" + @attributes=""AdditionalAttributes""> @code { - [Parameter] public string Value { get; set; } = """"; - [Parameter] public string Placeholder { get; set; } = """"; - [Parameter] public bool Disabled { get; set; } - [Parameter] public bool ReadOnly { get; set; } - [Parameter] public EventCallback ValueChanged { get; set; } - [Parameter] public EventCallback OnFocus { get; set; } - [Parameter] public EventCallback OnBlur { get; set; } - [Parameter(CaptureUnmatchedValues = true)] - public Dictionary? AdditionalAttributes { get; set; } + [Parameter] + public string? Value { get; set; } - private bool _isFocused = false; + [Parameter] + public EventCallback ValueChanged { get; set; } - private string CssClass => BuildCssClass(); + [Parameter] + public string? Placeholder { get; set; } - private string BuildCssClass() - { - var classes = new List - { - ""flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm"", - ""ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none"", - ""focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"", - ""disabled:cursor-not-allowed disabled:opacity-50"" - }; + [Parameter] + public bool Disabled { get; set; } - return string.Join("" "", classes); - } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } private async Task HandleInput(ChangeEventArgs e) { - var newValue = e.Value?.ToString() ?? """"; - Value = newValue; - await ValueChanged.InvokeAsync(newValue); - } - - private async Task HandleFocus(FocusEventArgs e) - { - _isFocused = true; - await OnFocus.InvokeAsync(e); - } - - private async Task HandleBlur(FocusEventArgs e) - { - _isFocused = false; - await OnBlur.InvokeAsync(e); + Value = e.Value?.ToString(); + await ValueChanged.InvokeAsync(Value); } } "; diff --git a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs index 0823916..a160b5b 100644 --- a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs +++ b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs @@ -22,21 +22,19 @@ @implements IAsyncDisposable + } + + @if (IsMobile) + { + + } +
+ + @if (SearchEnabled) + { + + } +
+ +@code { + [Parameter] public string Title { get; set; } = "Docs"; + [Parameter] public string? Logo { get; set; } + [Parameter] public bool SearchEnabled { get; set; } = true; + [Parameter] public bool CollapsibleControl { get; set; } = true; + [Parameter] public bool IsMobile { get; set; } = false; + + private void ToggleCollapsed() + { + SidebarService.ToggleCollapsed(); + } + + private void Close() + { + SidebarService.Close(); + } +} +``` + +### 4. SidebarViewport.razor + +```razor + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } +} +``` + +### 5. SidebarPageTree.razor + +```razor +@inject NavigationManager Navigation + +@foreach (var node in PageTree.Children) +{ + @RenderNode(node, 1) +} + +@code { + [Parameter] public PageTree PageTree { get; set; } = null!; + + private RenderFragment RenderNode(PageTreeNode node, int level) => builder => + { + if (node is PageTreeItem item) + { + + @item.Name + + } + else if (node is PageTreeFolder folder) + { + + @foreach (var child in folder.Children) + { + @RenderNode(child, level + 1) + } + + } + else if (node is PageTreeSeparator separator) + { + + } + }; + + private bool IsActive(string url) + { + var currentPath = new Uri(Navigation.Uri).AbsolutePath; + return currentPath.StartsWith(url, StringComparison.OrdinalIgnoreCase); + } +} +``` + +### 6. SidebarItem.razor + +```razor +@inject NavigationManager Navigation + + + + @if (!string.IsNullOrEmpty(Icon)) + { + + } + + @ChildContent + + +@code { + [Parameter] public string Url { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public bool IsActive { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + + [CascadingParameter] public SidebarService? SidebarService { get; set; } + + private void OnClick() + { + // Close mobile sidebar on navigation + if (SidebarService != null) + { + SidebarService.Close(); + } + } +} +``` + +### 7. SidebarFolder.razor + +```razor +@inject NavigationManager Navigation + + + +@code { + [Parameter] public string Name { get; set; } = ""; + [Parameter] public string? Icon { get; set; } + [Parameter] public bool DefaultOpen { get; set; } = false; + [Parameter] public int Level { get; set; } = 1; + [Parameter] public RenderFragment? ChildContent { get; set; } + + private bool IsOpen { get; set; } + + protected override void OnInitialized() + { + IsOpen = DefaultOpen; + } + + private void Toggle() + { + IsOpen = !IsOpen; + } +} +``` + +### 8. SidebarSeparator.razor + +```razor + + +@code { + [Parameter] public string? Name { get; set; } + [Parameter] public string? Icon { get; set; } +} +``` + +### 9. SidebarFooter.razor + +```razor + + +@code { + [Parameter] public List Links { get; set; } = new(); + [Parameter] public bool ThemeToggleEnabled { get; set; } = true; + [Parameter] public bool IsMobile { get; set; } = false; +} +``` + +--- + +## 🧭 Navigation Components + +### 1. Navbar.razor (Mobile Top Bar) + +```razor +@inject SidebarService SidebarService + +
+ + +
+ +@code { + [Parameter] public string Title { get; set; } = "Docs"; + [Parameter] public string? Logo { get; set; } + [Parameter] public bool SearchEnabled { get; set; } = true; + + private void ToggleSidebar() + { + SidebarService.Toggle(); + } +} +``` + +### 2. CollapsibleControl.razor (Desktop Collapse Button) + +```razor +@inject SidebarService SidebarService + +@if (SidebarService.IsCollapsed) +{ +
+ + + @if (SearchEnabled) + { + + } +
+} + +@code { + [Parameter] public bool SearchEnabled { get; set; } = true; + + protected override void OnInitialized() + { + SidebarService.OnStateChanged += StateHasChanged; + } + + private void Expand() + { + SidebarService.Expand(); + } + + public void Dispose() + { + SidebarService.OnStateChanged -= StateHasChanged; + } +} +``` + +--- + +## 🎨 CSS & Styling + +### 1. Main Stylesheet (wwwroot/css/shelldocs.css) + +```css +@import 'tailwindcss'; + +/* ======================================== + ShellDocs Design Tokens + ======================================== */ + +@theme { + /* Layout Variables */ + --sd-sidebar-width: 268px; + --sd-toc-width: 240px; + --sd-page-width: 1200px; + --sd-layout-width: 1600px; + --sd-nav-height: 56px; + --sd-banner-height: 0px; + --sd-tocnav-height: 0px; + + /* Mobile Sidebar */ + --sd-sidebar-mobile-offset: 100%; + + /* Colors - Light Mode (Neutral Black & White) */ + --color-sd-background: hsl(0, 0%, 96%); + --color-sd-foreground: hsl(0, 0%, 3.9%); + --color-sd-muted: hsl(0, 0%, 96.1%); + --color-sd-muted-foreground: hsl(0, 0%, 45.1%); + --color-sd-popover: hsl(0, 0%, 98%); + --color-sd-popover-foreground: hsl(0, 0%, 15.1%); + --color-sd-card: hsl(0, 0%, 94.7%); + --color-sd-card-foreground: hsl(0, 0%, 3.9%); + --color-sd-border: hsla(0, 0%, 80%, 50%); + --color-sd-primary: hsl(0, 0%, 9%); + --color-sd-primary-foreground: hsl(0, 0%, 98%); + --color-sd-secondary: hsl(0, 0%, 93.1%); + --color-sd-secondary-foreground: hsl(0, 0%, 9%); + --color-sd-accent: hsla(0, 0%, 82%, 50%); + --color-sd-accent-foreground: hsl(0, 0%, 9%); + --color-sd-ring: hsl(0, 0%, 63.9%); + + /* Animations */ + --animate-sd-fade-in: sd-fade-in 300ms ease; + --animate-sd-fade-out: sd-fade-out 300ms ease; + --animate-sd-sidebar-in: sd-sidebar-in 250ms ease; + --animate-sd-sidebar-out: sd-sidebar-out 250ms ease; + --animate-sd-collapsible-down: sd-collapsible-down 150ms cubic-bezier(0.45, 0, 0.55, 1); + --animate-sd-collapsible-up: sd-collapsible-up 150ms cubic-bezier(0.45, 0, 0.55, 1); +} + +/* Dark Mode */ +.dark { + --color-sd-background: hsl(0, 0%, 7.04%); + --color-sd-foreground: hsl(0, 0%, 92%); + --color-sd-muted: hsl(0, 0%, 12.9%); + --color-sd-muted-foreground: hsla(0, 0%, 70%, 0.8%); + --color-sd-popover: hsl(0, 0%, 11.6%); + --color-sd-popover-foreground: hsl(0, 0%, 86.9%); + --color-sd-card: hsl(0, 0%, 9.8%); + --color-sd-card-foreground: hsl(0, 0%, 98%); + --color-sd-border: hsla(0, 0%, 40%, 20%); + --color-sd-primary: hsl(0, 0%, 98%); + --color-sd-primary-foreground: hsl(0, 0%, 9%); + --color-sd-secondary: hsl(0, 0%, 12.9%); + --color-sd-secondary-foreground: hsl(0, 0%, 92%); + --color-sd-accent: hsla(0, 0%, 40.9%, 30%); + --color-sd-accent-foreground: hsl(0, 0%, 90%); + --color-sd-ring: hsl(0, 0%, 54.9%); + --color-sd-overlay: hsla(0, 0%, 0%, 0.2); +} + +/* Dark sidebar tweaks */ +.dark #shelldocs-sidebar { + --color-sd-muted: hsl(0, 0%, 16%); + --color-sd-secondary: hsl(0, 0%, 18%); + --color-sd-muted-foreground: hsl(0, 0%, 72%); +} + +/* RTL Support */ +[dir='rtl'] { + --sd-sidebar-mobile-offset: -100%; +} + +/* ======================================== + Animations + ======================================== */ + +@keyframes sd-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes sd-fade-out { + from { opacity: 1; } + to { opacity: 0; } +} + +@keyframes sd-sidebar-in { + from { transform: translateX(var(--sd-sidebar-mobile-offset)); } + to { transform: translateX(0); } +} + +@keyframes sd-sidebar-out { + from { transform: translateX(0); } + to { transform: translateX(var(--sd-sidebar-mobile-offset)); } +} + +@keyframes sd-collapsible-down { + from { + height: 0; + opacity: 0; + } + to { + height: var(--content-height); + opacity: 1; + } +} + +@keyframes sd-collapsible-up { + from { + height: var(--content-height); + opacity: 1; + } + to { + height: 0; + opacity: 0; + } +} + +/* ======================================== + Base Styles + ======================================== */ + +@layer base { + *, + ::before, + ::after, + ::backdrop { + border-color: var(--color-sd-border); + } + + body { + @apply min-h-screen flex flex-col; + background-color: var(--color-sd-background); + color: var(--color-sd-foreground); + } + + :root, + #shelldocs-content { + --sd-layout-offset: max(calc(50vw - var(--sd-layout-width) / 2), 0px); + } +} + +/* ======================================== + Layout Components + ======================================== */ + +/* Main Layout Container */ +.shelldocs-layout { + @apply relative min-h-screen flex; +} + +/* Main Content Wrapper */ +.shelldocs-main-wrapper { + @apply flex flex-1 flex-col; + padding-top: var(--sd-nav-height); + transition: padding 200ms; +} + +.shelldocs-content { + @apply flex-1 w-full mx-auto max-w-[var(--sd-page-width)]; + padding-inline-end: var(--sd-toc-width); + transition: padding-inline-start 200ms; +} + +/* When sidebar is collapsed */ +.sidebar-collapsed .shelldocs-content { + margin-inline: var(--sd-layout-offset); +} + +/* ======================================== + Desktop Sidebar + ======================================== */ + +.shelldocs-sidebar { + @apply fixed left-0 flex flex-col items-end z-20 bg-sd-card text-sm border-e; + top: calc(var(--sd-banner-height) + var(--sd-nav-height)); + bottom: 0; + width: calc(var(--spacing) + var(--sd-sidebar-width) + var(--sd-layout-offset)); + transition: all 200ms; +} + +.shelldocs-sidebar.collapsed { + @apply rounded-xl border shadow-lg; + width: var(--sd-sidebar-width); + top: calc(var(--sd-banner-height) + var(--sd-nav-height) + var(--sd-sidebar-margin, 0px)); + bottom: var(--sd-sidebar-margin, 0px); + transform: translateX(var(--sd-sidebar-offset, 0)); + opacity: 0; + pointer-events: none; +} + +.shelldocs-sidebar.collapsed:hover { + @apply z-50 opacity-100; + pointer-events: auto; +} + +.shelldocs-sidebar .sidebar-wrapper { + @apply flex flex-col h-full; + width: var(--sd-sidebar-width); +} + +/* Mobile: Hide sidebar */ +@media (max-width: 768px) { + .shelldocs-sidebar { + @apply hidden; + } +} + +/* ======================================== + Mobile Sidebar (Drawer) + ======================================== */ + +.sidebar-overlay { + @apply fixed inset-0 z-40 backdrop-blur-sm; + background-color: var(--color-sd-overlay); + animation: var(--animate-sd-fade-in); +} + +.sidebar-overlay[data-state='closed'] { + animation: var(--animate-sd-fade-out); +} + +.shelldocs-sidebar-mobile { + @apply fixed flex flex-col shadow-lg border-s end-0 inset-y-0 z-40 bg-sd-background; + width: 85%; + max-width: 380px; + text-size: 15px; + transform: translateX(var(--sd-sidebar-mobile-offset)); + transition: transform 250ms ease; +} + +.shelldocs-sidebar-mobile.open { + transform: translateX(0); + animation: var(--animate-sd-sidebar-in); +} + +.shelldocs-sidebar-mobile.closed { + animation: var(--animate-sd-sidebar-out); +} + +/* Desktop: Hide mobile sidebar */ +@media (min-width: 768px) { + .shelldocs-sidebar-mobile, + .sidebar-overlay { + @apply hidden; + } +} + +/* ======================================== + Sidebar Header + ======================================== */ + +.sidebar-header { + @apply flex flex-col gap-3 p-4 pb-2; +} + +.sidebar-header-content { + @apply flex flex-row items-center gap-2; +} + +.sidebar-logo { + @apply inline-flex items-center gap-2.5 font-medium flex-1; + font-size: 15px; +} + +.sidebar-logo .logo-image { + @apply h-6 w-auto; +} + +.sidebar-collapse-btn, +.sidebar-close-btn { + @apply p-1.5 rounded-lg transition-colors; + @apply text-sd-muted-foreground; + @apply hover:bg-sd-accent/50 hover:text-sd-accent-foreground; +} + +.sidebar-collapse-btn .icon, +.sidebar-close-btn .icon { + @apply w-5 h-5; +} + +.sidebar-search { + @apply w-full; +} + +/* ======================================== + Sidebar Viewport (Scrollable Area) + ======================================== */ + +.sidebar-viewport { + @apply flex-1 h-full overflow-hidden; +} + +.sidebar-scroll-area { + @apply h-full overflow-y-auto p-4; + --sidebar-item-offset: calc(var(--spacing) * 2); + + /* Gradient mask at top/bottom */ + mask-image: linear-gradient( + to bottom, + transparent, + white 12px, + white calc(100% - 12px), + transparent + ); +} + +/* Custom scrollbar */ +.sidebar-scroll-area::-webkit-scrollbar { + width: 5px; +} + +.sidebar-scroll-area::-webkit-scrollbar-thumb { + @apply rounded-full; + background: var(--color-sd-border); +} + +.sidebar-scroll-area::-webkit-scrollbar-track { + @apply bg-transparent; +} + +/* ======================================== + Sidebar Items + ======================================== */ + +.sidebar-item { + @apply relative flex items-center gap-2 rounded-lg p-2 text-start; + @apply text-sd-muted-foreground transition-colors; + padding-inline-start: var(--sidebar-item-offset); + overflow-wrap: anywhere; +} + +.sidebar-item:hover { + @apply bg-sd-accent/50 text-sd-accent-foreground/80; +} + +.sidebar-item.active { + @apply bg-sd-primary/10 text-sd-primary; +} + +.sidebar-item-icon { + @apply w-4 h-4 shrink-0; +} + +/* ======================================== + Sidebar Folder + ======================================== */ + +.sidebar-folder { + @apply relative; +} + +.sidebar-folder-trigger { + @apply relative flex w-full items-center gap-2 rounded-lg p-2 text-start; + @apply text-sd-muted-foreground transition-colors; + padding-inline-start: var(--sidebar-item-offset); +} + +.sidebar-folder-trigger:hover { + @apply bg-sd-accent/50 text-sd-accent-foreground/80; +} + +.sidebar-folder-chevron { + @apply w-4 h-4 ms-auto transition-transform shrink-0; +} + +.sidebar-folder-chevron:not(.open) { + @apply -rotate-90; +} + +.sidebar-folder-icon { + @apply w-4 h-4 shrink-0; +} + +.sidebar-folder-content { + @apply relative; + animation: var(--animate-sd-collapsible-down); +} + +/* Active indicator line for nested items */ +.sidebar-folder-content[data-level="1"]::before { + content: ''; + @apply absolute w-px inset-y-1 bg-sd-border; + left: 10px; +} + +.sidebar-folder-content[data-level="1"] .sidebar-item.active::before { + content: ''; + @apply absolute w-px inset-y-2.5 bg-sd-primary; + left: 10px; +} + +/* ======================================== + Sidebar Separator + ======================================== */ + +.sidebar-separator { + @apply inline-flex items-center gap-2 mb-1.5 px-2; + padding-inline-start: var(--sidebar-item-offset); +} + +.sidebar-separator:not(:first-child) { + @apply mt-6; +} + +.sidebar-separator-icon { + @apply w-4 h-4 shrink-0; +} + +/* ======================================== + Sidebar Footer + ======================================== */ + +.sidebar-footer { + @apply flex flex-col border-t p-4 pt-2; +} + +.sidebar-footer-content { + @apply flex items-center gap-2; + @apply text-sd-muted-foreground; +} + +.sidebar-footer-link { + @apply p-2 rounded-lg transition-colors; + @apply hover:bg-sd-accent/50 hover:text-sd-accent-foreground; +} + +/* ======================================== + Mobile Navbar + ======================================== */ + +.shelldocs-navbar { + @apply fixed top-0 left-0 right-0 z-30; + @apply flex items-center px-4 py-2 border-b; + @apply bg-sd-background/80 backdrop-blur-sm; + height: var(--sd-nav-height); +} + +.navbar-content { + @apply flex items-center justify-between w-full; +} + +.navbar-logo { + @apply inline-flex items-center gap-2.5 font-semibold; +} + +.navbar-logo .logo-image { + @apply h-6 w-auto; +} + +.navbar-actions { + @apply flex items-center gap-2; +} + +.navbar-menu-btn { + @apply p-2 rounded-lg transition-colors; + @apply text-sd-muted-foreground; + @apply hover:bg-sd-accent/50 hover:text-sd-accent-foreground; +} + +.navbar-menu-btn .icon { + @apply w-5 h-5; +} + +/* ======================================== + Collapsible Control (Desktop) + ======================================== */ + +.collapsible-control { + @apply fixed flex shadow-lg transition-opacity rounded-xl p-0.5 border; + @apply bg-sd-muted text-sd-muted-foreground z-10; + @apply max-md:hidden xl:start-4 max-xl:end-4; + top: calc(var(--sd-banner-height) + var(--sd-tocnav-height) + var(--spacing) * 4); +} + +.collapsible-control .collapse-btn { + @apply p-2 rounded-lg transition-colors; + @apply hover:bg-sd-accent/50 hover:text-sd-accent-foreground; +} + +/* ======================================== + Responsive Breakpoints + ======================================== */ + +/* Mobile: Full width content */ +@media (max-width: 768px) { + .shelldocs-content { + @apply w-full; + padding-top: var(--sd-nav-height); + padding-inline-start: 0; + padding-inline-end: 0; + } +} + +/* Tablet: Sidebar + Content */ +@media (min-width: 768px) and (max-width: 1280px) { + .shelldocs-content { + padding-inline-end: 0; + } +} + +/* Desktop: Sidebar + Content + TOC */ +@media (min-width: 1280px) { + .shelldocs-content { + padding-inline-end: var(--sd-toc-width); + } +} + +/* ======================================== + Utility Classes + ======================================== */ + +@utility sd-scroll-container { + &::-webkit-scrollbar { + width: 5px; + height: 5px; + } + + &::-webkit-scrollbar-thumb { + @apply rounded-full; + background: var(--color-sd-border); + } + + &::-webkit-scrollbar-track { + @apply bg-transparent; + } +} + +/* ======================================== + Print Styles + ======================================== */ + +@media print { + .shelldocs-sidebar, + .shelldocs-sidebar-mobile, + .shelldocs-navbar, + .sidebar-footer, + .collapsible-control { + @apply hidden; + } + + .shelldocs-content { + @apply w-full max-w-none; + padding: 0; + } +} +``` + +--- + +## 🔧 JavaScript Interop + +### wwwroot/js/shelldocs.js + +```javascript +// ShellDocs JavaScript Interop +window.ShellDocs = { + // Theme management + Theme: { + getTheme: function() { + const stored = localStorage.getItem('shelldocs-theme'); + if (stored) return stored; + + // Check system preference + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } + return 'light'; + }, + + setTheme: function(theme) { + localStorage.setItem('shelldocs-theme', theme); + + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, + + initializeTheme: function() { + const theme = this.getTheme(); + this.setTheme(theme); + } + }, + + // Sidebar management + Sidebar: { + lockBodyScroll: function(lock) { + if (lock) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + } + }, + + // Scroll utilities + Scroll: { + scrollToElement: function(elementId, offset = 0) { + const element = document.getElementById(elementId); + if (element) { + const top = element.getBoundingClientRect().top + window.pageYOffset - offset; + window.scrollTo({ top, behavior: 'smooth' }); + } + }, + + getScrollY: function() { + return window.scrollY || window.pageYOffset; + } + }, + + // Initialize on load + initialize: function() { + this.Theme.initializeTheme(); + } +}; + +// Auto-initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => window.ShellDocs.initialize()); +} else { + window.ShellDocs.initialize(); +} +``` + +--- + +## 📝 Model Classes + +### Models/PageTree.cs + +```csharp +namespace ShellDocs.Blazor.Models; + +public class PageTree +{ + public string Name { get; set; } = ""; + public List Children { get; set; } = new(); +} + +public abstract class PageTreeNode +{ + public string Name { get; set; } = ""; + public string? Icon { get; set; } +} + +public class PageTreeItem : PageTreeNode +{ + public string Url { get; set; } = ""; + public string? Description { get; set; } + public bool External { get; set; } +} + +public class PageTreeFolder : PageTreeNode +{ + public List Children { get; set; } = new(); + public bool DefaultOpen { get; set; } + public PageTreeItem? Index { get; set; } +} + +public class PageTreeSeparator : PageTreeNode +{ + // Separator has no additional properties +} +``` + +### Models/NavLink.cs + +```csharp +namespace ShellDocs.Blazor.Models; + +public class NavLink +{ + public string Url { get; set; } = ""; + public string Label { get; set; } = ""; + public string? Icon { get; set; } + public bool External { get; set; } +} +``` + +### Models/TocItem.cs + +```csharp +namespace ShellDocs.Blazor.Models; + +public class TocItem +{ + public int Level { get; set; } + public string Text { get; set; } = ""; + public string Url { get; set; } = ""; +} +``` + +--- + +## 🚀 Usage Example + +### Pages/Docs.razor + +```razor +@page "/docs/{*slug}" +@layout DocsLayout +@inject IContentService ContentService + +@page?.Title - Documentation + + + +
+

@page?.Title

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

@page.Description

+ } + + @((MarkupString)page?.Html) +
+
+ +@code { + [Parameter] public string? Slug { get; set; } + + private Page? page; + private PageTree pageTree = new(); + private List tocItems = new(); + private List links = new() + { + new NavLink { Url = "https://github.com/yourorg/shelldocs", Icon = "github", Label = "GitHub", External = true }, + new NavLink { Url = "https://twitter.com/yourorg", Icon = "twitter", Label = "Twitter", External = true } + }; + + protected override async Task OnParametersSetAsync() + { + var slugParts = Slug?.Split('/') ?? Array.Empty(); + page = await ContentService.GetPageAsync(slugParts); + pageTree = await ContentService.GetPageTreeAsync(); + tocItems = page?.Toc ?? new(); + } +} +``` + +--- + +## ✅ Checklist for Implementation + +### Core Services +- [ ] Create `SidebarService.cs` +- [ ] Create `NavigationService.cs` +- [ ] Create `ThemeService.cs` +- [ ] Register services in `Program.cs` + +### Layout Components +- [ ] Create `DocsLayout.razor` +- [ ] Create desktop `Sidebar.razor` +- [ ] Create mobile `SidebarMobile.razor` +- [ ] Create `Navbar.razor` +- [ ] Create `CollapsibleControl.razor` + +### Sidebar Components +- [ ] Create `SidebarHeader.razor` +- [ ] Create `SidebarFooter.razor` +- [ ] Create `SidebarViewport.razor` +- [ ] Create `SidebarPageTree.razor` +- [ ] Create `SidebarItem.razor` +- [ ] Create `SidebarFolder.razor` +- [ ] Create `SidebarSeparator.razor` + +### Styling +- [ ] Create `shelldocs.css` with design tokens +- [ ] Define animations +- [ ] Implement responsive breakpoints +- [ ] Test dark mode +- [ ] Add print styles + +### JavaScript +- [ ] Create `shelldocs.js` +- [ ] Implement theme management +- [ ] Add scroll utilities +- [ ] Test browser compatibility + +### Models +- [ ] Create `PageTree.cs` models +- [ ] Create `NavLink.cs` +- [ ] Create `TocItem.cs` + +### Testing +- [ ] Test responsive design +- [ ] Test mobile drawer +- [ ] Test sidebar collapse +- [ ] Test theme toggle +- [ ] Test navigation +- [ ] Test keyboard accessibility + +--- + +## 🎯 Key Features Implemented + +✅ **Responsive Design** - Mobile, tablet, desktop layouts +✅ **Sidebar** - Collapsible, hoverable when collapsed +✅ **Mobile Drawer** - Slide-in overlay with backdrop +✅ **Dark Mode** - Theme toggle with persistence +✅ **Smooth Animations** - Fade, slide, collapse transitions +✅ **Active States** - Current page highlighting +✅ **Nested Navigation** - Collapsible folders with indicators +✅ **Accessibility** - ARIA labels, keyboard navigation +✅ **Performance** - CSS-based animations, efficient rendering + +--- + +## 📚 Additional Notes + +### Browser Compatibility +- Modern browsers (Chrome, Firefox, Safari, Edge) +- CSS Grid and Flexbox +- CSS Variables (custom properties) +- CSS Animations + +### Performance Tips +- Use `@key` directive in loops +- Implement virtual scrolling for large trees +- Lazy load JavaScript interop +- Minimize re-renders with `ShouldRender()` + +### Customization +All design tokens are CSS variables, making it easy to: +- Change colors +- Adjust spacing +- Modify animations +- Create custom themes + +--- + +**This guide provides everything needed to build a production-ready fumadocs-style layout in Blazor!** 🚀 + From eb33fc8d318fda567ac456330a3b127432ef3187 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 15 Oct 2025 22:47:05 +0200 Subject: [PATCH 29/45] refactor: Update SVG paths in ThemeToggle component for improved icon representation in light and dark modes --- NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor | 4 ++-- src/ShellUI.Components/Components/ThemeToggle.razor | 4 ++-- src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor index 571c650..3ed11fa 100644 --- a/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor +++ b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor @@ -11,13 +11,13 @@ @if (_isDark) { - + } else { - + } diff --git a/src/ShellUI.Components/Components/ThemeToggle.razor b/src/ShellUI.Components/Components/ThemeToggle.razor index 190628a..abe9aae 100644 --- a/src/ShellUI.Components/Components/ThemeToggle.razor +++ b/src/ShellUI.Components/Components/ThemeToggle.razor @@ -14,13 +14,13 @@ @if (_isDark) { - + } else { - + } diff --git a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs index a160b5b..82e8fdf 100644 --- a/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs +++ b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs @@ -29,13 +29,13 @@ @implements IAsyncDisposable @if (_isDark) { - + } else { - + } From a3c1927c613605d6dddc97362b4970f56c98bb15 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 15 Oct 2025 22:52:54 +0200 Subject: [PATCH 30/45] feat: Implement ComponentManager for managing installed components, including listing, updating, and removing components, along with enhancements to the ComponentInstaller for configuration handling --- .../Services/ComponentInstaller.cs | 23 ++ src/ShellUI.CLI/Services/ComponentManager.cs | 227 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 src/ShellUI.CLI/Services/ComponentManager.cs diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs index 417392a..d8b1fc6 100644 --- a/src/ShellUI.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -71,7 +71,30 @@ public static void InstallComponents(string[] components, bool force) AnsiConsole.MarkupLine($"[red]Failed: {string.Join(", ", failedComponents)}[/]"); } + public static void InstallComponent(string componentName, ComponentMetadata metadata, bool force, bool skipConfig = false) + { + var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); + var configJson = File.ReadAllText(configPath); + var config = JsonSerializer.Deserialize(configJson); + + if (config == null) return; + + var projectInfo = ProjectDetector.DetectProject(); + var result = InstallComponentInternal(componentName, config, projectInfo, force); + + if (!skipConfig && result == InstallResult.Success) + { + var updatedJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(configPath, updatedJson); + } + } + private static InstallResult InstallComponent(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force) + { + return InstallComponentInternal(componentName, config, projectInfo, force); + } + + private static InstallResult InstallComponentInternal(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force) { if (!ComponentRegistry.Exists(componentName)) { diff --git a/src/ShellUI.CLI/Services/ComponentManager.cs b/src/ShellUI.CLI/Services/ComponentManager.cs new file mode 100644 index 0000000..e884810 --- /dev/null +++ b/src/ShellUI.CLI/Services/ComponentManager.cs @@ -0,0 +1,227 @@ +using System.Text.Json; +using Spectre.Console; +using ShellUI.Core.Models; +using ShellUI.Templates; + +namespace ShellUI.CLI.Services; + +public static class ComponentManager +{ + private const string ConfigFileName = "shellui.json"; + + public static List GetInstalledComponents() + { + if (!File.Exists(ConfigFileName)) + { + return new List(); + } + + try + { + var json = File.ReadAllText(ConfigFileName); + var config = JsonSerializer.Deserialize(json); + return config?.InstalledComponents?.Select(c => c.Name).ToList() ?? new List(); + } + catch + { + return new List(); + } + } + + public static void ListComponents(bool showOnlyInstalled, bool showOnlyAvailable) + { + var installed = GetInstalledComponents(); + + AnsiConsole.Write(new FigletText("ShellUI").Color(Color.Blue)); + AnsiConsole.MarkupLine("[dim]Available Components[/]\n"); + + var table = new Table(); + table.Border(TableBorder.Rounded); + table.AddColumn(new TableColumn("[bold]Component[/]").Centered()); + table.AddColumn(new TableColumn("[bold]Status[/]").Centered()); + table.AddColumn(new TableColumn("[bold]Version[/]").Centered()); + table.AddColumn(new TableColumn("[bold]Category[/]").Centered()); + table.AddColumn(new TableColumn("[bold]Description[/]")); + + int installedCount = 0; + int availableCount = 0; + + foreach (var component in ComponentRegistry.Components.Values.OrderBy(c => c.Category).ThenBy(c => c.Name)) + { + if (!component.IsAvailable) continue; + + bool isInstalled = installed.Contains(component.Name); + + if (showOnlyInstalled && !isInstalled) continue; + if (showOnlyAvailable && isInstalled) continue; + + var status = isInstalled + ? "[green]Installed[/]" + : "[dim]Available[/]"; + + var componentName = isInstalled + ? $"[green]{component.Name}[/]" + : component.Name; + + table.AddRow( + componentName, + status, + $"[dim]{component.Version}[/]", + $"[blue]{component.Category}[/]", + $"[dim]{component.Description}[/]" + ); + + if (isInstalled) installedCount++; + else availableCount++; + } + + AnsiConsole.Write(table); + + AnsiConsole.WriteLine(); + var panel = new Panel( + new Markup($"[green]{installedCount} installed[/] | [blue]{availableCount} available[/] | [yellow]{ComponentRegistry.Components.Count} total[/]") + ); + panel.Header = new PanelHeader("[bold]Summary[/]"); + panel.Border = BoxBorder.Rounded; + AnsiConsole.Write(panel); + + if (!showOnlyInstalled && !showOnlyAvailable) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[dim]Tip: Use 'dotnet shellui add ' to install a component[/]"); + } + } + + public static void RemoveComponents(string[] componentNames) + { + if (!File.Exists(ConfigFileName)) + { + AnsiConsole.MarkupLine("[red]Error:[/] ShellUI not initialized. Run 'dotnet shellui init' first."); + return; + } + + var installed = GetInstalledComponents(); + var componentsPath = "Components/UI"; + + if (!Directory.Exists(componentsPath)) + { + AnsiConsole.MarkupLine("[red]Error:[/] Components directory not found."); + return; + } + + AnsiConsole.WriteLine(); + AnsiConsole.Write(new Rule("[red]Removing Components[/]").RuleStyle("red dim")); + AnsiConsole.WriteLine(); + + foreach (var componentName in componentNames) + { + var normalizedName = componentName.ToLower().Replace(",", "").Trim(); + + if (!ComponentRegistry.Components.TryGetValue(normalizedName, out var metadata)) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] Unknown component '{componentName}'"); + continue; + } + + if (!installed.Contains(normalizedName)) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] Component '{metadata.DisplayName}' is not installed"); + continue; + } + + var componentPath = Path.Combine(componentsPath, metadata.FilePath); + + if (File.Exists(componentPath)) + { + File.Delete(componentPath); + AnsiConsole.MarkupLine($"[green]Removed:[/] {metadata.DisplayName}"); + + installed.Remove(normalizedName); + } + else + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] File not found: {componentPath}"); + } + } + + UpdateConfig(installed); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[green]Successfully removed {componentNames.Length} component(s)[/]"); + } + + public static void UpdateComponents(string[] componentNames, bool updateAll) + { + if (!File.Exists(ConfigFileName)) + { + AnsiConsole.MarkupLine("[red]Error:[/] ShellUI not initialized. Run 'dotnet shellui init' first."); + return; + } + + var installed = GetInstalledComponents(); + + if (installed.Count == 0) + { + AnsiConsole.MarkupLine("[yellow]No components installed yet.[/]"); + return; + } + + var toUpdate = updateAll || componentNames.Length == 0 + ? installed.ToArray() + : componentNames; + + AnsiConsole.WriteLine(); + AnsiConsole.Write(new Rule("[blue]Updating Components[/]").RuleStyle("blue dim")); + AnsiConsole.WriteLine(); + + foreach (var componentName in toUpdate) + { + var normalizedName = componentName.ToLower().Replace(",", "").Trim(); + + if (!ComponentRegistry.Components.TryGetValue(normalizedName, out var metadata)) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] Unknown component '{componentName}'"); + continue; + } + + if (!installed.Contains(normalizedName)) + { + AnsiConsole.MarkupLine($"[yellow]Skipped:[/] Component '{metadata.DisplayName}' is not installed"); + continue; + } + + ComponentInstaller.InstallComponent(normalizedName, metadata, force: true, skipConfig: true); + AnsiConsole.MarkupLine($"[green]Updated:[/] {metadata.DisplayName} to v{metadata.Version}"); + } + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[green]Successfully updated {toUpdate.Length} component(s)[/]"); + } + + private static void UpdateConfig(List installedComponentNames) + { + try + { + var json = File.ReadAllText(ConfigFileName); + var config = JsonSerializer.Deserialize(json); + + if (config != null) + { + config.InstalledComponents = config.InstalledComponents + .Where(c => installedComponentNames.Contains(c.Name)) + .ToList(); + + var updatedJson = JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(ConfigFileName, updatedJson); + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] Failed to update config: {ex.Message}"); + } + } +} + From 353a8bc27688dad18a69f94956acd5f9bdd26d30 Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 15 Oct 2025 22:53:27 +0200 Subject: [PATCH 31/45] fix: Add error handling for component management commands to improve user feedback and stability --- src/ShellUI.CLI/Program.cs | 54 +++++++++++--------------------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/src/ShellUI.CLI/Program.cs b/src/ShellUI.CLI/Program.cs index 2989544..10a72f2 100644 --- a/src/ShellUI.CLI/Program.cs +++ b/src/ShellUI.CLI/Program.cs @@ -104,33 +104,14 @@ static Command CreateListCommand() command.SetHandler((installed, available) => { - AnsiConsole.MarkupLine("[blue]ShellUI Components[/]\n"); - - var table = new Table(); - table.AddColumn("Component"); - table.AddColumn("Version"); - table.AddColumn("Category"); - table.AddColumn("Description"); - - // Get components from registry - foreach (var component in ComponentRegistry.Components.Values) + try { - if (component.IsAvailable) - { - table.AddRow( - component.Name, - $"[dim]{component.Version}[/]", - component.Category.ToString(), - component.Description - ); - } + ComponentManager.ListComponents(installed, available); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); } - - AnsiConsole.Write(table); - - var totalCount = ComponentRegistry.Components.Count; - AnsiConsole.MarkupLine($"\n[green]{totalCount} component(s) available[/]"); - AnsiConsole.MarkupLine("[yellow]More coming soon: Target 40+ components![/]"); }, installedOption, availableOption); return command; @@ -150,12 +131,14 @@ static Command CreateRemoveCommand() command.SetHandler((components) => { - AnsiConsole.MarkupLine("[red]Removing components:[/]"); - foreach (var component in components) + try { - AnsiConsole.MarkupLine($" - {component}"); + ComponentManager.RemoveComponents(components); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); } - AnsiConsole.MarkupLine("\n[yellow]Coming soon in Milestone 1![/]"); }, componentsArg); return command; @@ -180,19 +163,14 @@ static Command CreateUpdateCommand() command.SetHandler((components, all) => { - if (all || components.Length == 0) + try { - AnsiConsole.MarkupLine("[blue]Updating all components...[/]"); + ComponentManager.UpdateComponents(components, all); } - else + catch (Exception ex) { - AnsiConsole.MarkupLine("[blue]Updating specific components:[/]"); - foreach (var component in components) - { - AnsiConsole.MarkupLine($" - {component}"); - } + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); } - AnsiConsole.MarkupLine("\n[yellow]Coming soon in Milestone 1![/]"); }, componentsArg, allOption); return command; From 850e107755239a5c752824e4b71b53c1cdc89d7d Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 01:26:55 +0200 Subject: [PATCH 32/45] feat: Add new UI components including Accordion, Avatar, Breadcrumb, Checkbox, and more to enhance the ShellUI component library, improving user interface options and functionality --- .../Components/UI/Accordion.razor | 28 +++ .../Components/UI/AccordionItem.razor | 53 +++++ .../Components/UI/Avatar.razor | 42 ++++ .../Components/UI/Breadcrumb.razor | 18 ++ .../Components/UI/BreadcrumbItem.razor | 40 ++++ .../Components/UI/Checkbox.razor | 43 ++++ .../Components/UI/Combobox.razor | 82 +++++++ .../Components/UI/DatePicker.razor | 128 +++++++++++ .../Components/UI/DateRangePicker.razor | 195 ++++++++++++++++ .../Components/UI/Dropdown.razor | 43 ++++ .../Components/UI/Form.razor | 28 +++ .../Components/UI/InputOTP.razor | 145 ++++++++++++ .../Components/UI/Navbar.razor | 18 ++ .../Components/UI/Pagination.razor | 70 ++++++ .../Components/UI/Popover.razor | 54 +++++ .../Components/UI/Progress.razor | 19 ++ .../Components/UI/RadioGroup.razor | 31 +++ .../Components/UI/RadioGroupItem.razor | 51 +++++ .../Components/UI/Select.razor | 36 +++ .../Components/UI/Separator.razor | 14 ++ .../Components/UI/Sidebar.razor | 21 ++ .../Components/UI/Skeleton.razor | 14 ++ .../Components/UI/Slider.razor | 47 ++++ .../Components/UI/Switch.razor | 38 ++++ .../Components/UI/Table.razor | 18 ++ .../Components/UI/TableBody.razor | 16 ++ .../Components/UI/TableCell.razor | 16 ++ .../Components/UI/TableHead.razor | 16 ++ .../Components/UI/TableHeader.razor | 16 ++ .../Components/UI/TableRow.razor | 16 ++ .../Components/UI/Tabs.razor | 24 ++ .../Components/UI/TimePicker.razor | 93 ++++++++ .../Components/UI/Toast.razor | 62 +++++ .../Components/UI/Toggle.razor | 41 ++++ .../Components/UI/Tooltip.razor | 37 +++ .../Templates/AccordionItemTemplate.cs | 73 ++++++ .../Templates/AccordionTemplate.cs | 48 ++++ .../Templates/AvatarTemplate.cs | 62 +++++ .../Templates/BreadcrumbItemTemplate.cs | 60 +++++ .../Templates/BreadcrumbTemplate.cs | 38 ++++ .../Templates/CheckboxTemplate.cs | 63 ++++++ .../Templates/CollapsibleTemplate.cs | 66 ++++++ .../Templates/ComboboxTemplate.cs | 102 +++++++++ .../Templates/DatePickerTemplate.cs | 148 ++++++++++++ .../Templates/DateRangePickerTemplate.cs | 214 ++++++++++++++++++ .../Templates/DrawerTemplate.cs | 80 +++++++ .../Templates/DropdownTemplate.cs | 63 ++++++ .../Templates/FormTemplate.cs | 48 ++++ .../Templates/InputOTPTemplate.cs | 165 ++++++++++++++ .../Templates/MenubarItemTemplate.cs | 46 ++++ .../Templates/MenubarTemplate.cs | 36 +++ .../Templates/NavbarTemplate.cs | 38 ++++ .../Templates/NavigationMenuItemTemplate.cs | 63 ++++++ .../Templates/NavigationMenuTemplate.cs | 36 +++ .../Templates/PaginationTemplate.cs | 90 ++++++++ .../Templates/PopoverTemplate.cs | 74 ++++++ .../Templates/ProgressTemplate.cs | 39 ++++ .../Templates/RadioGroupItemTemplate.cs | 71 ++++++ .../Templates/RadioGroupTemplate.cs | 51 +++++ .../Templates/ResizableTemplate.cs | 36 +++ .../Templates/ScrollAreaTemplate.cs | 41 ++++ .../Templates/SelectTemplate.cs | 56 +++++ .../Templates/SeparatorTemplate.cs | 34 +++ .../Templates/SheetTemplate.cs | 75 ++++++ .../Templates/SidebarTemplate.cs | 41 ++++ .../Templates/SkeletonTemplate.cs | 34 +++ .../Templates/SliderTemplate.cs | 67 ++++++ .../Templates/SwitchTemplate.cs | 58 +++++ .../Templates/TableBodyTemplate.cs | 36 +++ .../Templates/TableCellTemplate.cs | 36 +++ .../Templates/TableHeadTemplate.cs | 36 +++ .../Templates/TableHeaderTemplate.cs | 36 +++ .../Templates/TableRowTemplate.cs | 36 +++ .../Templates/TableTemplate.cs | 38 ++++ .../Templates/TabsTemplate.cs | 44 ++++ .../Templates/TimePickerTemplate.cs | 113 +++++++++ .../Templates/ToastTemplate.cs | 83 +++++++ .../Templates/ToggleTemplate.cs | 62 +++++ .../Templates/TooltipTemplate.cs | 57 +++++ 79 files changed, 4406 insertions(+) create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Accordion.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/AccordionItem.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Avatar.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Breadcrumb.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/BreadcrumbItem.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Checkbox.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Combobox.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/DatePicker.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/DateRangePicker.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Dropdown.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Form.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/InputOTP.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Navbar.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Pagination.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Popover.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Progress.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/RadioGroup.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/RadioGroupItem.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Select.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Separator.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Skeleton.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Slider.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Switch.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Table.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/TableBody.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/TableCell.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/TableHead.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/TableHeader.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/TableRow.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Tabs.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Toast.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Toggle.razor create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Tooltip.razor create mode 100644 src/ShellUI.Templates/Templates/AccordionItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AccordionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AvatarTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BreadcrumbTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CheckboxTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CollapsibleTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/ComboboxTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DatePickerTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DrawerTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DropdownTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/FormTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/InputOTPTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/MenubarItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/MenubarTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/NavbarTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/NavigationMenuItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/NavigationMenuTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PaginationTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PopoverTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/ProgressTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/RadioGroupItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/RadioGroupTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/ResizableTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/ScrollAreaTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SelectTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SeparatorTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SheetTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SidebarTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SkeletonTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SliderTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SwitchTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TableBodyTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TableCellTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TableHeadTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TableHeaderTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TableRowTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TableTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TabsTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TimePickerTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/ToastTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/ToggleTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/TooltipTemplate.cs diff --git a/NET9/BlazorInteractiveServer/Components/UI/Accordion.razor b/NET9/BlazorInteractiveServer/Components/UI/Accordion.razor new file mode 100644 index 0000000..f7a6af1 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Accordion.razor @@ -0,0 +1,28 @@ +@namespace BlazorInteractiveServer.Components.UI + +
+ @ChildContent +
+ +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private List _items = new(); + + public void RegisterItem(AccordionItem item) + { + _items.Add(item); + } + + public void UnregisterItem(AccordionItem item) + { + _items.Remove(item); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/AccordionItem.razor b/NET9/BlazorInteractiveServer/Components/UI/AccordionItem.razor new file mode 100644 index 0000000..08fdae2 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/AccordionItem.razor @@ -0,0 +1,53 @@ +@namespace BlazorInteractiveServer.Components.UI + +
+ + @if (IsOpen) + { +
+ @ChildContent +
+ } +
+ +@code { + [CascadingParameter] + private Accordion? Accordion { get; set; } + + [Parameter] + public string Title { get; set; } = ""; + + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + protected override void OnInitialized() + { + Accordion?.RegisterItem(this); + } + + public void Dispose() + { + Accordion?.UnregisterItem(this); + } + + private void Toggle() + { + IsOpen = !IsOpen; + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Avatar.razor b/NET9/BlazorInteractiveServer/Components/UI/Avatar.razor new file mode 100644 index 0000000..b9facaf --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Avatar.razor @@ -0,0 +1,42 @@ +@namespace BlazorInteractiveServer.Components.UI + + + @if (!string.IsNullOrEmpty(Src)) + { + @Alt + } + else if (!string.IsNullOrEmpty(Fallback)) + { + + @Fallback + + } + else + { + + + + + + } + + +@code { + [Parameter] + public string? Src { get; set; } + + [Parameter] + public string? Alt { get; set; } + + [Parameter] + public string? Fallback { get; set; } + + [Parameter] + public string Size { get; set; } = "default"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Breadcrumb.razor b/NET9/BlazorInteractiveServer/Components/UI/Breadcrumb.razor new file mode 100644 index 0000000..37c5dc7 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Breadcrumb.razor @@ -0,0 +1,18 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/BreadcrumbItem.razor b/NET9/BlazorInteractiveServer/Components/UI/BreadcrumbItem.razor new file mode 100644 index 0000000..3b495d5 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/BreadcrumbItem.razor @@ -0,0 +1,40 @@ +@namespace BlazorInteractiveServer.Components.UI + +
  • + @if (!string.IsNullOrEmpty(Href)) + { + + @ChildContent + + } + else + { + + @ChildContent + + } + + @if (!IsLast) + { + + + + } +
  • + +@code { + [Parameter] + public string? Href { get; set; } + + [Parameter] + public bool IsLast { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Checkbox.razor b/NET9/BlazorInteractiveServer/Components/UI/Checkbox.razor new file mode 100644 index 0000000..22a1f4c --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Checkbox.razor @@ -0,0 +1,43 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public bool Checked { get; set; } + + [Parameter] + public EventCallback CheckedChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (!Disabled) + { + Checked = !Checked; + await CheckedChanged.InvokeAsync(Checked); + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Combobox.razor b/NET9/BlazorInteractiveServer/Components/UI/Combobox.razor new file mode 100644 index 0000000..835c66f --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Combobox.razor @@ -0,0 +1,82 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + + + @if (_isOpen) + { +
    +
    + +
    +
    + @foreach (var item in FilteredOptions) + { +
    + @item +
    + } + @if (!FilteredOptions.Any()) + { +
    No results found.
    + } +
    +
    + } +
    + +@code { + [Parameter] + public string SelectedValue { get; set; } = ""; + + [Parameter] + public EventCallback SelectedValueChanged { get; set; } + + [Parameter] + public List Options { get; set; } = new(); + + [Parameter] + public string Placeholder { get; set; } = "Select option..."; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private string _searchQuery = ""; + + private IEnumerable FilteredOptions => + string.IsNullOrWhiteSpace(_searchQuery) + ? Options + : Options.Where(o => o.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase)); + + private void ToggleOpen() => _isOpen = !_isOpen; + + private async Task SelectOption(string option) + { + SelectedValue = option; + _isOpen = false; + _searchQuery = ""; + await SelectedValueChanged.InvokeAsync(option); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/DatePicker.razor b/NET9/BlazorInteractiveServer/Components/UI/DatePicker.razor new file mode 100644 index 0000000..67b6237 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/DatePicker.razor @@ -0,0 +1,128 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + + } + + + +
    + + + @if (_isOpen) + { +
    +
    + +
    @_currentMonth.ToString("MMMM yyyy")
    + +
    +
    +
    Su
    +
    Mo
    +
    Tu
    +
    We
    +
    Th
    +
    Fr
    +
    Sa
    + @foreach (var day in GetCalendarDays()) + { + @if (day.HasValue) + { + + } + else + { +
    + } + } +
    +
    + } +
    + +@code { + [Parameter] + public DateTime? SelectedDate { get; set; } + + [Parameter] + public EventCallback SelectedDateChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Pick a date"; + + [Parameter] + public bool AllowClear { get; set; } = true; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private DateTime _currentMonth = DateTime.Now; + + private void ToggleCalendar() => _isOpen = !_isOpen; + private void PreviousMonth() => _currentMonth = _currentMonth.AddMonths(-1); + private void NextMonth() => _currentMonth = _currentMonth.AddMonths(1); + + private async Task SelectDate(DateTime date) + { + SelectedDate = date; + _isOpen = false; + await SelectedDateChanged.InvokeAsync(date); + } + + private async Task ClearDate() + { + SelectedDate = null; + _isOpen = false; + await SelectedDateChanged.InvokeAsync(null); + } + + private List GetCalendarDays() + { + var days = new List(); + var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); + var lastDay = firstDay.AddMonths(1).AddDays(-1); + var startDayOfWeek = (int)firstDay.DayOfWeek; + + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); + for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); + + return days; + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/DateRangePicker.razor b/NET9/BlazorInteractiveServer/Components/UI/DateRangePicker.razor new file mode 100644 index 0000000..1f64938 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/DateRangePicker.razor @@ -0,0 +1,195 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + + } + + + +
    + + + @if (_isOpen) + { +
    +
    + +
    @_currentMonth.ToString("MMMM yyyy")
    + +
    +
    +
    Su
    +
    Mo
    +
    Tu
    +
    We
    +
    Th
    +
    Fr
    +
    Sa
    + @foreach (var day in GetCalendarDays()) + { + @if (day.HasValue) + { + + } + else + { +
    + } + } +
    +
    + } +
    + +@code { + [Parameter] + public DateTime? StartDate { get; set; } + + [Parameter] + public EventCallback StartDateChanged { get; set; } + + [Parameter] + public DateTime? EndDate { get; set; } + + [Parameter] + public EventCallback EndDateChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Pick a date range"; + + [Parameter] + public bool AllowClear { get; set; } = true; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private DateTime _currentMonth = DateTime.Now; + private bool _selectingStart = true; + + private string GetDisplayText() + { + if (StartDate.HasValue && EndDate.HasValue) + { + return $"{StartDate.Value:MMM dd} - {EndDate.Value:MMM dd, yyyy}"; + } + else if (StartDate.HasValue) + { + return $"{StartDate.Value:MMM dd, yyyy} - ..."; + } + return Placeholder; + } + + private string GetDateButtonClass(DateTime date) + { + var baseClass = "h-8 w-8 text-sm rounded-md hover:bg-accent transition-colors"; + + if (StartDate.HasValue && EndDate.HasValue) + { + if (date.Date == StartDate.Value.Date || date.Date == EndDate.Value.Date) + { + return baseClass + " bg-primary text-primary-foreground"; + } + if (date > StartDate.Value && date < EndDate.Value) + { + return baseClass + " bg-primary/20 text-primary-foreground"; + } + } + else if (StartDate.HasValue && date.Date == StartDate.Value.Date) + { + return baseClass + " bg-primary text-primary-foreground"; + } + + return baseClass; + } + + private void ToggleCalendar() => _isOpen = !_isOpen; + private void PreviousMonth() => _currentMonth = _currentMonth.AddMonths(-1); + private void NextMonth() => _currentMonth = _currentMonth.AddMonths(1); + + private async Task SelectDate(DateTime date) + { + if (_selectingStart || (!StartDate.HasValue && !EndDate.HasValue)) + { + StartDate = date; + EndDate = null; + _selectingStart = false; + await StartDateChanged.InvokeAsync(date); + } + else + { + if (date < StartDate) + { + EndDate = StartDate; + StartDate = date; + await StartDateChanged.InvokeAsync(date); + await EndDateChanged.InvokeAsync(EndDate); + } + else + { + EndDate = date; + await EndDateChanged.InvokeAsync(date); + } + _isOpen = false; + _selectingStart = true; + } + } + + private async Task ClearRange() + { + StartDate = null; + EndDate = null; + _selectingStart = true; + _isOpen = false; + await StartDateChanged.InvokeAsync(null); + await EndDateChanged.InvokeAsync(null); + } + + private List GetCalendarDays() + { + var days = new List(); + var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); + var lastDay = firstDay.AddMonths(1).AddDays(-1); + var startDayOfWeek = (int)firstDay.DayOfWeek; + + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); + for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); + + return days; + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Dropdown.razor b/NET9/BlazorInteractiveServer/Components/UI/Dropdown.razor new file mode 100644 index 0000000..7bbfd7b --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Dropdown.razor @@ -0,0 +1,43 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@code { + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task ToggleOpen() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Form.razor b/NET9/BlazorInteractiveServer/Components/UI/Form.razor new file mode 100644 index 0000000..1d7e451 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Form.razor @@ -0,0 +1,28 @@ +@namespace BlazorInteractiveServer.Components.UI +@using Microsoft.AspNetCore.Components.Forms + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnValidSubmit { get; set; } + + [Parameter] + public EventCallback OnInvalidSubmit { get; set; } + + [Parameter] + public string ClassName { get; set; } = "space-y-6"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleSubmit() + { + await Task.CompletedTask; + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/InputOTP.razor b/NET9/BlazorInteractiveServer/Components/UI/InputOTP.razor new file mode 100644 index 0000000..61dbcd3 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/InputOTP.razor @@ -0,0 +1,145 @@ +@namespace BlazorInteractiveServer.Components.UI +@using Microsoft.JSInterop +@inject IJSRuntime JS + +
    + @for (int i = 0; i < Length; i++) + { + var index = i; + var isGroupStart = GroupBy > 0 && i % GroupBy == 0; + var isGroupEnd = GroupBy > 0 && (i + 1) % GroupBy == 0; + + @if (isGroupStart && i > 0) + { + - + } + + + } +
    + +@code { + [Parameter] + public string Value { get; set; } = ""; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public EventCallback OnComplete { get; set; } + + [Parameter] + public int Length { get; set; } = 6; + + [Parameter] + public int GroupBy { get; set; } = 3; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private int _focusedIndex = -1; + private ElementReference[] _inputRefs = null!; + private string _id = Guid.NewGuid().ToString("N")[..8]; + + protected override void OnInitialized() + { + _inputRefs = new ElementReference[Length]; + } + + private string GetDigit(int index) => index < Value.Length ? Value[index].ToString() : ""; + + private async Task HandleInput(int index, ChangeEventArgs e) + { + var input = e.Value?.ToString() ?? ""; + if (string.IsNullOrEmpty(input) || !char.IsDigit(input[0])) + { + return; + } + + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index] = input[0]; + Value = new string(chars).TrimEnd(); + + await ValueChanged.InvokeAsync(Value); + + if (index < Length - 1) + { + await FocusInput(index + 1); + } + + if (Value.Replace(" ", "").Length == Length) + { + await OnComplete.InvokeAsync(Value); + } + } + + private async Task HandleKeyDown(int index, KeyboardEventArgs e) + { + if (e.Key == "Backspace") + { + if (string.IsNullOrEmpty(GetDigit(index)) && index > 0) + { + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index - 1] = ' '; + Value = new string(chars).TrimEnd(); + await ValueChanged.InvokeAsync(Value); + await FocusInput(index - 1); + } + else if (!string.IsNullOrEmpty(GetDigit(index))) + { + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index] = ' '; + Value = new string(chars).TrimEnd(); + await ValueChanged.InvokeAsync(Value); + } + } + else if (e.Key == "ArrowLeft" && index > 0) + { + await FocusInput(index - 1); + } + else if (e.Key == "ArrowRight" && index < Length - 1) + { + await FocusInput(index + 1); + } + } + + private async Task HandlePaste(ClipboardEventArgs e) + { + await Task.CompletedTask; + } + + private async Task FocusInput(int index) + { + if (index >= 0 && index < Length) + { + try + { + await JS.InvokeVoidAsync("eval", $"document.getElementById('otp-input-{_id}-{index}')?.focus()"); + } + catch + { + } + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Navbar.razor b/NET9/BlazorInteractiveServer/Components/UI/Navbar.razor new file mode 100644 index 0000000..a82d2da --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Navbar.razor @@ -0,0 +1,18 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Pagination.razor b/NET9/BlazorInteractiveServer/Components/UI/Pagination.razor new file mode 100644 index 0000000..5c316c9 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Pagination.razor @@ -0,0 +1,70 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public int CurrentPage { get; set; } = 1; + + [Parameter] + public int TotalPages { get; set; } = 1; + + [Parameter] + public EventCallback OnPageChange { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task GoToPage(int page) + { + if (page >= 1 && page <= TotalPages && page != CurrentPage) + { + CurrentPage = page; + await OnPageChange.InvokeAsync(page); + } + } + + private Task Previous() => GoToPage(CurrentPage - 1); + private Task Next() => GoToPage(CurrentPage + 1); +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Popover.razor b/NET9/BlazorInteractiveServer/Components/UI/Popover.razor new file mode 100644 index 0000000..06b7da5 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Popover.razor @@ -0,0 +1,54 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    +
    + @Trigger +
    + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Placement { get; set; } = "bottom"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Toggle() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private async Task Close() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(IsOpen); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Progress.razor b/NET9/BlazorInteractiveServer/Components/UI/Progress.razor new file mode 100644 index 0000000..2bec0a4 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Progress.razor @@ -0,0 +1,19 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    +
    +
    + +@code { + [Parameter] + public int Value { get; set; } = 0; + + [Parameter] + public string Height { get; set; } = "0.5rem"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/RadioGroup.razor b/NET9/BlazorInteractiveServer/Components/UI/RadioGroup.razor new file mode 100644 index 0000000..40a64d4 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/RadioGroup.razor @@ -0,0 +1,31 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] + public string? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + public async Task SetValue(string value) + { + if (Value != value) + { + Value = value; + await ValueChanged.InvokeAsync(value); + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/RadioGroupItem.razor b/NET9/BlazorInteractiveServer/Components/UI/RadioGroupItem.razor new file mode 100644 index 0000000..452a2f2 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/RadioGroupItem.razor @@ -0,0 +1,51 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + + @if (ChildContent != null) + { + + } +
    + +@code { + [CascadingParameter] + private RadioGroup? RadioGroup { get; set; } + + [Parameter] + public string Value { get; set; } = ""; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsChecked => RadioGroup?.Value == Value; + + private async Task OnClick() + { + if (RadioGroup != null) + { + await RadioGroup.SetValue(Value); + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Select.razor b/NET9/BlazorInteractiveServer/Components/UI/Select.razor new file mode 100644 index 0000000..748c216 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Select.razor @@ -0,0 +1,36 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public string? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleChange(ChangeEventArgs e) + { + Value = e.Value?.ToString(); + await ValueChanged.InvokeAsync(Value); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Separator.razor b/NET9/BlazorInteractiveServer/Components/UI/Separator.razor new file mode 100644 index 0000000..aa1b104 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Separator.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + +@code { + [Parameter] + public string Orientation { get; set; } = "horizontal"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor b/NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor new file mode 100644 index 0000000..5790e30 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Sidebar.razor @@ -0,0 +1,21 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public bool IsOpen { get; set; } = true; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Skeleton.razor b/NET9/BlazorInteractiveServer/Components/UI/Skeleton.razor new file mode 100644 index 0000000..ade08ff --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Skeleton.razor @@ -0,0 +1,14 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + +@code { + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Slider.razor b/NET9/BlazorInteractiveServer/Components/UI/Slider.razor new file mode 100644 index 0000000..0c431b3 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Slider.razor @@ -0,0 +1,47 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + +
    + +@code { + [Parameter] + public double Value { get; set; } = 50; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public double Min { get; set; } = 0; + + [Parameter] + public double Max { get; set; } = 100; + + [Parameter] + public double Step { get; set; } = 1; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleInput(ChangeEventArgs args) + { + if (double.TryParse(args.Value?.ToString(), out var newValue)) + { + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Switch.razor b/NET9/BlazorInteractiveServer/Components/UI/Switch.razor new file mode 100644 index 0000000..f2d988c --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Switch.razor @@ -0,0 +1,38 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public bool Checked { get; set; } + + [Parameter] + public EventCallback CheckedChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (!Disabled) + { + Checked = !Checked; + await CheckedChanged.InvokeAsync(Checked); + } + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Table.razor b/NET9/BlazorInteractiveServer/Components/UI/Table.razor new file mode 100644 index 0000000..ca3ac58 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Table.razor @@ -0,0 +1,18 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + + @ChildContent +
    +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/TableBody.razor b/NET9/BlazorInteractiveServer/Components/UI/TableBody.razor new file mode 100644 index 0000000..78b6b55 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/TableBody.razor @@ -0,0 +1,16 @@ +@namespace BlazorInteractiveServer.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/TableCell.razor b/NET9/BlazorInteractiveServer/Components/UI/TableCell.razor new file mode 100644 index 0000000..2568c34 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/TableCell.razor @@ -0,0 +1,16 @@ +@namespace BlazorInteractiveServer.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/TableHead.razor b/NET9/BlazorInteractiveServer/Components/UI/TableHead.razor new file mode 100644 index 0000000..ef9f402 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/TableHead.razor @@ -0,0 +1,16 @@ +@namespace BlazorInteractiveServer.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/TableHeader.razor b/NET9/BlazorInteractiveServer/Components/UI/TableHeader.razor new file mode 100644 index 0000000..9abd0ad --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/TableHeader.razor @@ -0,0 +1,16 @@ +@namespace BlazorInteractiveServer.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/TableRow.razor b/NET9/BlazorInteractiveServer/Components/UI/TableRow.razor new file mode 100644 index 0000000..7ee0cb4 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/TableRow.razor @@ -0,0 +1,16 @@ +@namespace BlazorInteractiveServer.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Tabs.razor b/NET9/BlazorInteractiveServer/Components/UI/Tabs.razor new file mode 100644 index 0000000..1f5728a --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Tabs.razor @@ -0,0 +1,24 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    +
    + @TabHeaders +
    +
    + @TabContent +
    +
    + +@code { + [Parameter] + public RenderFragment? TabHeaders { get; set; } + + [Parameter] + public RenderFragment? TabContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor b/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor new file mode 100644 index 0000000..6ef3fdd --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor @@ -0,0 +1,93 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + + + @if (_isOpen) + { +
    +
    +
    + + +
    +
    + + +
    +
    + +
    + } +
    + +@code { + [Parameter] + public TimeSpan? SelectedTime { get; set; } + + [Parameter] + public EventCallback SelectedTimeChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Pick a time"; + + [Parameter] + public int Step { get; set; } = 15; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private string selectedHour = "0"; + private string selectedMinute = "0"; + + protected override void OnParametersSet() + { + if (SelectedTime.HasValue) + { + selectedHour = SelectedTime.Value.Hours.ToString(); + selectedMinute = SelectedTime.Value.Minutes.ToString(); + } + } + + private void TogglePicker() => _isOpen = !_isOpen; + + private async Task ApplyTime() + { + var hour = int.Parse(selectedHour); + var minute = int.Parse(selectedMinute); + SelectedTime = new TimeSpan(hour, minute, 0); + _isOpen = false; + await SelectedTimeChanged.InvokeAsync(SelectedTime); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Toast.razor b/NET9/BlazorInteractiveServer/Components/UI/Toast.razor new file mode 100644 index 0000000..e1f864a --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Toast.razor @@ -0,0 +1,62 @@ +@namespace BlazorInteractiveServer.Components.UI + +@if (IsVisible) +{ +
    +
    +
    + @if (!string.IsNullOrEmpty(Title)) + { +
    @Title
    + } + @if (!string.IsNullOrEmpty(Description)) + { +
    @Description
    + } + @ChildContent +
    + +
    +
    +} + +@code { + [Parameter] + public bool IsVisible { get; set; } + + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Description { get; set; } + + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string Position { get; set; } = "bottom-right"; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + IsVisible = false; + await IsVisibleChanged.InvokeAsync(IsVisible); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Toggle.razor b/NET9/BlazorInteractiveServer/Components/UI/Toggle.razor new file mode 100644 index 0000000..a72fe79 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Toggle.razor @@ -0,0 +1,41 @@ +@namespace BlazorInteractiveServer.Components.UI + + + +@code { + [Parameter] + public bool Pressed { get; set; } + + [Parameter] + public EventCallback PressedChanged { get; set; } + + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string Size { get; set; } = "default"; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + Pressed = !Pressed; + await PressedChanged.InvokeAsync(Pressed); + } +} diff --git a/NET9/BlazorInteractiveServer/Components/UI/Tooltip.razor b/NET9/BlazorInteractiveServer/Components/UI/Tooltip.razor new file mode 100644 index 0000000..84a9d24 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Tooltip.razor @@ -0,0 +1,37 @@ +@namespace BlazorInteractiveServer.Components.UI + +
    + @Trigger + + @if (_isVisible) + { +
    + @Content +
    + } +
    + +@code { + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? Content { get; set; } + + [Parameter] + public string Placement { get; set; } = "top"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isVisible; + + private void Show() => _isVisible = true; + private void Hide() => _isVisible = false; +} diff --git a/src/ShellUI.Templates/Templates/AccordionItemTemplate.cs b/src/ShellUI.Templates/Templates/AccordionItemTemplate.cs new file mode 100644 index 0000000..160b5a0 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AccordionItemTemplate.cs @@ -0,0 +1,73 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AccordionItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "accordion-item", + DisplayName = "Accordion Item", + Description = "Individual collapsible section within an Accordion", + Category = ComponentCategory.Layout, + FilePath = "AccordionItem.razor", + Version = "0.1.0", + Tags = new List { "layout", "collapsible", "item" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@code { + [CascadingParameter] + private Accordion? Accordion { get; set; } + + [Parameter] + public string Title { get; set; } = """"; + + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + protected override void OnInitialized() + { + Accordion?.RegisterItem(this); + } + + public void Dispose() + { + Accordion?.UnregisterItem(this); + } + + private void Toggle() + { + IsOpen = !IsOpen; + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/AccordionTemplate.cs b/src/ShellUI.Templates/Templates/AccordionTemplate.cs new file mode 100644 index 0000000..e79208d --- /dev/null +++ b/src/ShellUI.Templates/Templates/AccordionTemplate.cs @@ -0,0 +1,48 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AccordionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "accordion", + DisplayName = "Accordion", + Description = "Collapsible content sections", + Category = ComponentCategory.Layout, + FilePath = "Accordion.razor", + Version = "0.1.0", + Tags = new List { "layout", "collapsible", "accordion" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private List _items = new(); + + public void RegisterItem(AccordionItem item) + { + _items.Add(item); + } + + public void UnregisterItem(AccordionItem item) + { + _items.Remove(item); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/AvatarTemplate.cs b/src/ShellUI.Templates/Templates/AvatarTemplate.cs new file mode 100644 index 0000000..c42c284 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AvatarTemplate.cs @@ -0,0 +1,62 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AvatarTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "avatar", + DisplayName = "Avatar", + Description = "User avatar image component", + Category = ComponentCategory.DataDisplay, + FilePath = "Avatar.razor", + Version = "0.1.0", + Tags = new List { "avatar", "image", "user", "profile" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @if (!string.IsNullOrEmpty(Src)) + { + + } + else if (!string.IsNullOrEmpty(Fallback)) + { + + @Fallback + + } + else + { + + + + + + } + + +@code { + [Parameter] + public string? Src { get; set; } + + [Parameter] + public string? Alt { get; set; } + + [Parameter] + public string? Fallback { get; set; } + + [Parameter] + public string Size { get; set; } = ""default""; + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs new file mode 100644 index 0000000..b584622 --- /dev/null +++ b/src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs @@ -0,0 +1,60 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class BreadcrumbItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "breadcrumb-item", + DisplayName = "Breadcrumb Item", + Description = "Individual breadcrumb item", + Category = ComponentCategory.Layout, + FilePath = "BreadcrumbItem.razor", + Version = "0.1.0", + Tags = new List { "navigation", "breadcrumb", "item" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
  • + @if (!string.IsNullOrEmpty(Href)) + { + + @ChildContent + + } + else + { + + @ChildContent + + } + + @if (!IsLast) + { + + + + } +
  • + +@code { + [Parameter] + public string? Href { get; set; } + + [Parameter] + public bool IsLast { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/BreadcrumbTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbTemplate.cs new file mode 100644 index 0000000..e9d0a73 --- /dev/null +++ b/src/ShellUI.Templates/Templates/BreadcrumbTemplate.cs @@ -0,0 +1,38 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class BreadcrumbTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "breadcrumb", + DisplayName = "Breadcrumb", + Description = "Navigation breadcrumb trail", + Category = ComponentCategory.Layout, + FilePath = "Breadcrumb.razor", + Version = "0.1.0", + Tags = new List { "navigation", "breadcrumb", "layout" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/CheckboxTemplate.cs b/src/ShellUI.Templates/Templates/CheckboxTemplate.cs new file mode 100644 index 0000000..dc1aad4 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CheckboxTemplate.cs @@ -0,0 +1,63 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class CheckboxTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "checkbox", + DisplayName = "Checkbox", + Description = "Checkbox input with checked state", + Category = ComponentCategory.Form, + Version = "0.1.0", + FilePath = "Checkbox.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public bool Checked { get; set; } + + [Parameter] + public EventCallback CheckedChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (!Disabled) + { + Checked = !Checked; + await CheckedChanged.InvokeAsync(Checked); + } + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/CollapsibleTemplate.cs b/src/ShellUI.Templates/Templates/CollapsibleTemplate.cs new file mode 100644 index 0000000..180ff5e --- /dev/null +++ b/src/ShellUI.Templates/Templates/CollapsibleTemplate.cs @@ -0,0 +1,66 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class CollapsibleTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "collapsible", + DisplayName = "Collapsible", + Description = "Collapsible content component", + Category = ComponentCategory.Layout, + FilePath = "Collapsible.razor", + Version = "0.1.0", + Tags = new List { "layout", "collapsible", "toggle", "expand" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string Title { get; set; } = ""Collapsible""; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Toggle() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/ComboboxTemplate.cs b/src/ShellUI.Templates/Templates/ComboboxTemplate.cs new file mode 100644 index 0000000..d433941 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ComboboxTemplate.cs @@ -0,0 +1,102 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class ComboboxTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "combobox", + DisplayName = "Combobox", + Description = "Searchable dropdown combobox component", + Category = ComponentCategory.Form, + FilePath = "Combobox.razor", + Version = "0.1.0", + Tags = new List { "form", "select", "dropdown", "search", "autocomplete" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + + @if (_isOpen) + { +
    +
    + +
    +
    + @foreach (var item in FilteredOptions) + { +
    SelectOption(item))"" + class=""@(""relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground "" + (item == SelectedValue ? ""bg-accent"" : """"))""> + @item +
    + } + @if (!FilteredOptions.Any()) + { +
    No results found.
    + } +
    +
    + } +
    + +@code { + [Parameter] + public string SelectedValue { get; set; } = """"; + + [Parameter] + public EventCallback SelectedValueChanged { get; set; } + + [Parameter] + public List Options { get; set; } = new(); + + [Parameter] + public string Placeholder { get; set; } = ""Select option...""; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private string _searchQuery = """"; + + private IEnumerable FilteredOptions => + string.IsNullOrWhiteSpace(_searchQuery) + ? Options + : Options.Where(o => o.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase)); + + private void ToggleOpen() => _isOpen = !_isOpen; + + private async Task SelectOption(string option) + { + SelectedValue = option; + _isOpen = false; + _searchQuery = """"; + await SelectedValueChanged.InvokeAsync(option); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/DatePickerTemplate.cs b/src/ShellUI.Templates/Templates/DatePickerTemplate.cs new file mode 100644 index 0000000..ca424b4 --- /dev/null +++ b/src/ShellUI.Templates/Templates/DatePickerTemplate.cs @@ -0,0 +1,148 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DatePickerTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "date-picker", + DisplayName = "Date Picker", + Description = "Calendar date picker component", + Category = ComponentCategory.Form, + FilePath = "DatePicker.razor", + Version = "0.1.0", + Tags = new List { "form", "date", "calendar", "input" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + } + + + +
    + + + @if (_isOpen) + { +
    +
    + +
    @_currentMonth.ToString(""MMMM yyyy"")
    + +
    +
    +
    Su
    +
    Mo
    +
    Tu
    +
    We
    +
    Th
    +
    Fr
    +
    Sa
    + @foreach (var day in GetCalendarDays()) + { + @if (day.HasValue) + { + + } + else + { +
    + } + } +
    +
    + } +
    + +@code { + [Parameter] + public DateTime? SelectedDate { get; set; } + + [Parameter] + public EventCallback SelectedDateChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = ""Pick a date""; + + [Parameter] + public bool AllowClear { get; set; } = true; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private DateTime _currentMonth = DateTime.Now; + + private void ToggleCalendar() => _isOpen = !_isOpen; + private void PreviousMonth() => _currentMonth = _currentMonth.AddMonths(-1); + private void NextMonth() => _currentMonth = _currentMonth.AddMonths(1); + + private async Task SelectDate(DateTime date) + { + SelectedDate = date; + _isOpen = false; + await SelectedDateChanged.InvokeAsync(date); + } + + private async Task ClearDate() + { + SelectedDate = null; + _isOpen = false; + await SelectedDateChanged.InvokeAsync(null); + } + + private List GetCalendarDays() + { + var days = new List(); + var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); + var lastDay = firstDay.AddMonths(1).AddDays(-1); + var startDayOfWeek = (int)firstDay.DayOfWeek; + + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); + for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); + + return days; + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs b/src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs new file mode 100644 index 0000000..7a3390c --- /dev/null +++ b/src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs @@ -0,0 +1,214 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DateRangePickerTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "date-range-picker", + DisplayName = "Date Range Picker", + Description = "Date range picker component for selecting start and end dates", + Category = ComponentCategory.Form, + FilePath = "DateRangePicker.razor", + Version = "0.1.0", + Tags = new List { "form", "date", "range", "calendar", "input" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + } + + + +
    + + + @if (_isOpen) + { +
    +
    + +
    @_currentMonth.ToString(""MMMM yyyy"")
    + +
    +
    +
    Su
    +
    Mo
    +
    Tu
    +
    We
    +
    Th
    +
    Fr
    +
    Sa
    + @foreach (var day in GetCalendarDays()) + { + @if (day.HasValue) + { + + } + else + { +
    + } + } +
    +
    + } +
    + +@code { + [Parameter] + public DateTime? StartDate { get; set; } + + [Parameter] + public EventCallback StartDateChanged { get; set; } + + [Parameter] + public DateTime? EndDate { get; set; } + + [Parameter] + public EventCallback EndDateChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = ""Pick a date range""; + + [Parameter] + public bool AllowClear { get; set; } = true; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private DateTime _currentMonth = DateTime.Now; + private bool _selectingStart = true; + + private string GetDisplayText() + { + if (StartDate.HasValue && EndDate.HasValue) + { + return $""{StartDate.Value:MMM dd} - {EndDate.Value:MMM dd, yyyy}""; + } + else if (StartDate.HasValue) + { + return $""{StartDate.Value:MMM dd, yyyy} - ...""; + } + return Placeholder; + } + + private string GetDateButtonClass(DateTime date) + { + var baseClass = ""h-8 w-8 text-sm rounded-md hover:bg-accent transition-colors""; + + if (StartDate.HasValue && EndDate.HasValue) + { + if (date.Date == StartDate.Value.Date || date.Date == EndDate.Value.Date) + { + return baseClass + "" bg-primary text-primary-foreground""; + } + if (date > StartDate.Value && date < EndDate.Value) + { + return baseClass + "" bg-primary/20 text-primary-foreground""; + } + } + else if (StartDate.HasValue && date.Date == StartDate.Value.Date) + { + return baseClass + "" bg-primary text-primary-foreground""; + } + + return baseClass; + } + + private void ToggleCalendar() => _isOpen = !_isOpen; + private void PreviousMonth() => _currentMonth = _currentMonth.AddMonths(-1); + private void NextMonth() => _currentMonth = _currentMonth.AddMonths(1); + + private async Task SelectDate(DateTime date) + { + if (_selectingStart || (!StartDate.HasValue && !EndDate.HasValue)) + { + StartDate = date; + EndDate = null; + _selectingStart = false; + await StartDateChanged.InvokeAsync(date); + } + else + { + if (date < StartDate) + { + EndDate = StartDate; + StartDate = date; + await StartDateChanged.InvokeAsync(date); + await EndDateChanged.InvokeAsync(EndDate); + } + else + { + EndDate = date; + await EndDateChanged.InvokeAsync(date); + } + _isOpen = false; + _selectingStart = true; + } + } + + private async Task ClearRange() + { + StartDate = null; + EndDate = null; + _selectingStart = true; + _isOpen = false; + await StartDateChanged.InvokeAsync(null); + await EndDateChanged.InvokeAsync(null); + } + + private List GetCalendarDays() + { + var days = new List(); + var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); + var lastDay = firstDay.AddMonths(1).AddDays(-1); + var startDayOfWeek = (int)firstDay.DayOfWeek; + + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); + for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); + + return days; + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/DrawerTemplate.cs b/src/ShellUI.Templates/Templates/DrawerTemplate.cs new file mode 100644 index 0000000..5d88d90 --- /dev/null +++ b/src/ShellUI.Templates/Templates/DrawerTemplate.cs @@ -0,0 +1,80 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DrawerTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "drawer", + DisplayName = "Drawer", + Description = "Sliding drawer component with handle", + Category = ComponentCategory.Overlay, + FilePath = "Drawer.razor", + Version = "0.1.0", + Tags = new List { "overlay", "drawer", "modal", "slide" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@if (IsOpen) +{ +
    +
    + +
    +
    +
    +
    +
    + @if (!string.IsNullOrEmpty(Title)) + { +
    +

    @Title

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

    @Description

    + } +
    + } +
    + @ChildContent +
    +
    +
    +
    +} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Description { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(false); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/DropdownTemplate.cs b/src/ShellUI.Templates/Templates/DropdownTemplate.cs new file mode 100644 index 0000000..a6c001e --- /dev/null +++ b/src/ShellUI.Templates/Templates/DropdownTemplate.cs @@ -0,0 +1,63 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class DropdownTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "dropdown", + DisplayName = "Dropdown", + Description = "Dropdown menu with trigger and content", + Category = ComponentCategory.Overlay, + Version = "0.1.0", + FilePath = "Dropdown.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@code { + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task ToggleOpen() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/FormTemplate.cs b/src/ShellUI.Templates/Templates/FormTemplate.cs new file mode 100644 index 0000000..f0210e0 --- /dev/null +++ b/src/ShellUI.Templates/Templates/FormTemplate.cs @@ -0,0 +1,48 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class FormTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "form", + DisplayName = "Form", + Description = "Form wrapper component with validation support", + Category = ComponentCategory.Form, + FilePath = "Form.razor", + Version = "0.1.0", + Tags = new List { "form", "validation", "input", "wrapper" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using Microsoft.AspNetCore.Components.Forms + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnValidSubmit { get; set; } + + [Parameter] + public EventCallback OnInvalidSubmit { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""space-y-6""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleSubmit() + { + await Task.CompletedTask; + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/InputOTPTemplate.cs b/src/ShellUI.Templates/Templates/InputOTPTemplate.cs new file mode 100644 index 0000000..52db5dd --- /dev/null +++ b/src/ShellUI.Templates/Templates/InputOTPTemplate.cs @@ -0,0 +1,165 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class InputOTPTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "input-otp", + DisplayName = "Input OTP", + Description = "One-time password input component", + Category = ComponentCategory.Form, + FilePath = "InputOTP.razor", + Version = "0.1.0", + Tags = new List { "form", "input", "otp", "password", "verification" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using Microsoft.JSInterop +@inject IJSRuntime JS + +
    + @for (int i = 0; i < Length; i++) + { + var index = i; + var isGroupStart = GroupBy > 0 && i % GroupBy == 0; + var isGroupEnd = GroupBy > 0 && (i + 1) % GroupBy == 0; + + @if (isGroupStart && i > 0) + { + - + } + + HandleInput(index, e))"" + @onkeydown=""@(e => HandleKeyDown(index, e))"" + @onpaste=""@HandlePaste"" + @onfocus=""@(() => _focusedIndex = index)"" + disabled=""@Disabled"" + class=""@(""w-10 h-12 text-center text-lg font-semibold border-2 rounded-md transition-colors "" + (Disabled ? ""opacity-50 cursor-not-allowed bg-muted"" : ""bg-background"") + "" "" + (_focusedIndex == index ? ""border-ring ring-2 ring-ring ring-offset-2"" : ""border-input"") + "" focus:outline-none focus:border-ring focus:ring-2 focus:ring-ring focus:ring-offset-2 "" + (isGroupEnd && i < Length - 1 ? ""mr-0"" : ""mr-1""))"" /> + } +
    + +@code { + [Parameter] + public string Value { get; set; } = """"; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public EventCallback OnComplete { get; set; } + + [Parameter] + public int Length { get; set; } = 6; + + [Parameter] + public int GroupBy { get; set; } = 3; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private int _focusedIndex = -1; + private ElementReference[] _inputRefs = null!; + private string _id = Guid.NewGuid().ToString(""N"")[..8]; + + protected override void OnInitialized() + { + _inputRefs = new ElementReference[Length]; + } + + private string GetDigit(int index) => index < Value.Length ? Value[index].ToString() : """"; + + private async Task HandleInput(int index, ChangeEventArgs e) + { + var input = e.Value?.ToString() ?? """"; + if (string.IsNullOrEmpty(input) || !char.IsDigit(input[0])) + { + return; + } + + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index] = input[0]; + Value = new string(chars).TrimEnd(); + + await ValueChanged.InvokeAsync(Value); + + if (index < Length - 1) + { + await FocusInput(index + 1); + } + + if (Value.Replace("" "", """").Length == Length) + { + await OnComplete.InvokeAsync(Value); + } + } + + private async Task HandleKeyDown(int index, KeyboardEventArgs e) + { + if (e.Key == ""Backspace"") + { + if (string.IsNullOrEmpty(GetDigit(index)) && index > 0) + { + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index - 1] = ' '; + Value = new string(chars).TrimEnd(); + await ValueChanged.InvokeAsync(Value); + await FocusInput(index - 1); + } + else if (!string.IsNullOrEmpty(GetDigit(index))) + { + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index] = ' '; + Value = new string(chars).TrimEnd(); + await ValueChanged.InvokeAsync(Value); + } + } + else if (e.Key == ""ArrowLeft"" && index > 0) + { + await FocusInput(index - 1); + } + else if (e.Key == ""ArrowRight"" && index < Length - 1) + { + await FocusInput(index + 1); + } + } + + private async Task HandlePaste(ClipboardEventArgs e) + { + await Task.CompletedTask; + } + + private async Task FocusInput(int index) + { + if (index >= 0 && index < Length) + { + try + { + await JS.InvokeVoidAsync(""eval"", $""document.getElementById('otp-input-{_id}-{index}')?.focus()""); + } + catch + { + } + } + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/MenubarItemTemplate.cs b/src/ShellUI.Templates/Templates/MenubarItemTemplate.cs new file mode 100644 index 0000000..be28fdb --- /dev/null +++ b/src/ShellUI.Templates/Templates/MenubarItemTemplate.cs @@ -0,0 +1,46 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class MenubarItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "menubar-item", + DisplayName = "Menubar Item", + Description = "Individual menubar item", + Category = ComponentCategory.Navigation, + FilePath = "MenubarItem.razor", + Version = "0.1.0", + Tags = new List { "navigation", "menu", "menubar", "item" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/MenubarTemplate.cs b/src/ShellUI.Templates/Templates/MenubarTemplate.cs new file mode 100644 index 0000000..bf42f24 --- /dev/null +++ b/src/ShellUI.Templates/Templates/MenubarTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class MenubarTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "menubar", + DisplayName = "Menubar", + Description = "Application menubar component", + Category = ComponentCategory.Navigation, + FilePath = "Menubar.razor", + Version = "0.1.0", + Tags = new List { "navigation", "menu", "menubar", "app" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/NavbarTemplate.cs b/src/ShellUI.Templates/Templates/NavbarTemplate.cs new file mode 100644 index 0000000..76f2bfb --- /dev/null +++ b/src/ShellUI.Templates/Templates/NavbarTemplate.cs @@ -0,0 +1,38 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class NavbarTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "navbar", + DisplayName = "Navbar", + Description = "Top navigation bar with backdrop blur", + Category = ComponentCategory.Layout, + Version = "0.1.0", + FilePath = "Navbar.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/NavigationMenuItemTemplate.cs b/src/ShellUI.Templates/Templates/NavigationMenuItemTemplate.cs new file mode 100644 index 0000000..fed7ed8 --- /dev/null +++ b/src/ShellUI.Templates/Templates/NavigationMenuItemTemplate.cs @@ -0,0 +1,63 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class NavigationMenuItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "navigation-menu-item", + DisplayName = "Navigation Menu Item", + Description = "Individual navigation menu item", + Category = ComponentCategory.Navigation, + FilePath = "NavigationMenuItem.razor", + Version = "0.1.0", + Tags = new List { "navigation", "menu", "nav", "item" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string? Href { get; set; } + + [Parameter] + public bool IsActive { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/NavigationMenuTemplate.cs b/src/ShellUI.Templates/Templates/NavigationMenuTemplate.cs new file mode 100644 index 0000000..6d39742 --- /dev/null +++ b/src/ShellUI.Templates/Templates/NavigationMenuTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class NavigationMenuTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "navigation-menu", + DisplayName = "Navigation Menu", + Description = "Navigation menu component", + Category = ComponentCategory.Navigation, + FilePath = "NavigationMenu.razor", + Version = "0.1.0", + Tags = new List { "navigation", "menu", "nav" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/PaginationTemplate.cs b/src/ShellUI.Templates/Templates/PaginationTemplate.cs new file mode 100644 index 0000000..b06965b --- /dev/null +++ b/src/ShellUI.Templates/Templates/PaginationTemplate.cs @@ -0,0 +1,90 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PaginationTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "pagination", + DisplayName = "Pagination", + Description = "Pagination component for navigating pages", + Category = ComponentCategory.Navigation, + FilePath = "Pagination.razor", + Version = "0.1.0", + Tags = new List { "navigation", "pagination", "pages" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public int CurrentPage { get; set; } = 1; + + [Parameter] + public int TotalPages { get; set; } = 1; + + [Parameter] + public EventCallback OnPageChange { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task GoToPage(int page) + { + if (page >= 1 && page <= TotalPages && page != CurrentPage) + { + CurrentPage = page; + await OnPageChange.InvokeAsync(page); + } + } + + private Task Previous() => GoToPage(CurrentPage - 1); + private Task Next() => GoToPage(CurrentPage + 1); +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/PopoverTemplate.cs b/src/ShellUI.Templates/Templates/PopoverTemplate.cs new file mode 100644 index 0000000..b54cb02 --- /dev/null +++ b/src/ShellUI.Templates/Templates/PopoverTemplate.cs @@ -0,0 +1,74 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PopoverTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "popover", + DisplayName = "Popover", + Description = "Popover content component", + Category = ComponentCategory.Overlay, + FilePath = "Popover.razor", + Version = "0.1.0", + Tags = new List { "popover", "overlay", "popup", "dropdown" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    +
    + @Trigger +
    + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Placement { get; set; } = ""bottom""; + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Toggle() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private async Task Close() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(IsOpen); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/ProgressTemplate.cs b/src/ShellUI.Templates/Templates/ProgressTemplate.cs new file mode 100644 index 0000000..8cfa1c2 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ProgressTemplate.cs @@ -0,0 +1,39 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class ProgressTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "progress", + DisplayName = "Progress", + Description = "Progress bar indicator with customizable height", + Category = ComponentCategory.Feedback, + Version = "0.1.0", + FilePath = "Progress.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    +
    +
    + +@code { + [Parameter] + public int Value { get; set; } = 0; + + [Parameter] + public string Height { get; set; } = ""0.5rem""; + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/RadioGroupItemTemplate.cs b/src/ShellUI.Templates/Templates/RadioGroupItemTemplate.cs new file mode 100644 index 0000000..08fcdd6 --- /dev/null +++ b/src/ShellUI.Templates/Templates/RadioGroupItemTemplate.cs @@ -0,0 +1,71 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class RadioGroupItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "radio-group-item", + DisplayName = "Radio Group Item", + Description = "Individual radio button within a Radio Group", + Category = ComponentCategory.Form, + FilePath = "RadioGroupItem.razor", + Version = "0.1.0", + Tags = new List { "form", "input", "radio", "item" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + @if (ChildContent != null) + { + + } +
    + +@code { + [CascadingParameter] + private RadioGroup? RadioGroup { get; set; } + + [Parameter] + public string Value { get; set; } = """"; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsChecked => RadioGroup?.Value == Value; + + private async Task OnClick() + { + if (RadioGroup != null) + { + await RadioGroup.SetValue(Value); + } + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/RadioGroupTemplate.cs b/src/ShellUI.Templates/Templates/RadioGroupTemplate.cs new file mode 100644 index 0000000..91ea401 --- /dev/null +++ b/src/ShellUI.Templates/Templates/RadioGroupTemplate.cs @@ -0,0 +1,51 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class RadioGroupTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "radio-group", + DisplayName = "Radio Group", + Description = "Radio button group with single selection", + Category = ComponentCategory.Form, + FilePath = "RadioGroup.razor", + Version = "0.1.0", + Tags = new List { "form", "input", "radio", "selection" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] + public string? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + public async Task SetValue(string value) + { + if (Value != value) + { + Value = value; + await ValueChanged.InvokeAsync(value); + } + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/ResizableTemplate.cs b/src/ShellUI.Templates/Templates/ResizableTemplate.cs new file mode 100644 index 0000000..8401d12 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ResizableTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class ResizableTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "resizable", + DisplayName = "Resizable", + Description = "Resizable panel component", + Category = ComponentCategory.Layout, + FilePath = "Resizable.razor", + Version = "0.1.0", + Tags = new List { "layout", "resizable", "panel", "split" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/ScrollAreaTemplate.cs b/src/ShellUI.Templates/Templates/ScrollAreaTemplate.cs new file mode 100644 index 0000000..91d1ee8 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ScrollAreaTemplate.cs @@ -0,0 +1,41 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class ScrollAreaTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "scroll-area", + DisplayName = "Scroll Area", + Description = "Custom scrollable container component", + Category = ComponentCategory.Layout, + FilePath = "ScrollArea.razor", + Version = "0.1.0", + Tags = new List { "layout", "scroll", "overflow", "container" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    +
    + @ChildContent +
    +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string MaxHeight { get; set; } = ""400px""; + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/SelectTemplate.cs b/src/ShellUI.Templates/Templates/SelectTemplate.cs new file mode 100644 index 0000000..5cc868b --- /dev/null +++ b/src/ShellUI.Templates/Templates/SelectTemplate.cs @@ -0,0 +1,56 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SelectTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "select", + DisplayName = "Select", + Description = "Dropdown select input component", + Category = ComponentCategory.Form, + Version = "0.1.0", + FilePath = "Select.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public string? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleChange(ChangeEventArgs e) + { + Value = e.Value?.ToString(); + await ValueChanged.InvokeAsync(Value); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/SeparatorTemplate.cs b/src/ShellUI.Templates/Templates/SeparatorTemplate.cs new file mode 100644 index 0000000..9f52296 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SeparatorTemplate.cs @@ -0,0 +1,34 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SeparatorTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "separator", + DisplayName = "Separator", + Description = "Horizontal or vertical divider line", + Category = ComponentCategory.Layout, + Version = "0.1.0", + FilePath = "Separator.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + +@code { + [Parameter] + public string Orientation { get; set; } = ""horizontal""; + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/SheetTemplate.cs b/src/ShellUI.Templates/Templates/SheetTemplate.cs new file mode 100644 index 0000000..7fb5c42 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SheetTemplate.cs @@ -0,0 +1,75 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class SheetTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sheet", + DisplayName = "Sheet", + Description = "Side panel/drawer component with multiple positions", + Category = ComponentCategory.Overlay, + FilePath = "Sheet.razor", + Version = "0.1.0", + Tags = new List { "overlay", "sheet", "drawer", "panel", "side" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@if (IsOpen) +{ +
    +
    + +
    +
    +
    +

    @Title

    + +
    +
    + @ChildContent +
    +
    +
    +
    +} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string Title { get; set; } = ""Sheet""; + + [Parameter] + public string Side { get; set; } = ""right""; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(false); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/SidebarTemplate.cs b/src/ShellUI.Templates/Templates/SidebarTemplate.cs new file mode 100644 index 0000000..19058e3 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SidebarTemplate.cs @@ -0,0 +1,41 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SidebarTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sidebar", + DisplayName = "Sidebar", + Description = "Side navigation panel with toggle", + Category = ComponentCategory.Layout, + Version = "0.1.0", + FilePath = "Sidebar.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public bool IsOpen { get; set; } = true; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/SkeletonTemplate.cs b/src/ShellUI.Templates/Templates/SkeletonTemplate.cs new file mode 100644 index 0000000..8f7b558 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SkeletonTemplate.cs @@ -0,0 +1,34 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SkeletonTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "skeleton", + DisplayName = "Skeleton", + Description = "Loading placeholder with pulse animation", + Category = ComponentCategory.Feedback, + Version = "0.1.0", + FilePath = "Skeleton.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + +@code { + [Parameter] + public string Variant { get; set; } = ""default""; + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/SliderTemplate.cs b/src/ShellUI.Templates/Templates/SliderTemplate.cs new file mode 100644 index 0000000..fac7d17 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SliderTemplate.cs @@ -0,0 +1,67 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class SliderTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "slider", + DisplayName = "Slider", + Description = "Range slider input component", + Category = ComponentCategory.Form, + FilePath = "Slider.razor", + Version = "0.1.0", + Tags = new List { "form", "input", "range", "slider" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + +
    + +@code { + [Parameter] + public double Value { get; set; } = 50; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public double Min { get; set; } = 0; + + [Parameter] + public double Max { get; set; } = 100; + + [Parameter] + public double Step { get; set; } = 1; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleInput(ChangeEventArgs args) + { + if (double.TryParse(args.Value?.ToString(), out var newValue)) + { + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/SwitchTemplate.cs b/src/ShellUI.Templates/Templates/SwitchTemplate.cs new file mode 100644 index 0000000..bb5f171 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SwitchTemplate.cs @@ -0,0 +1,58 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class SwitchTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "switch", + DisplayName = "Switch", + Description = "Toggle switch for boolean values", + Category = ComponentCategory.Form, + Version = "0.1.0", + FilePath = "Switch.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public bool Checked { get; set; } + + [Parameter] + public EventCallback CheckedChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (!Disabled) + { + Checked = !Checked; + await CheckedChanged.InvokeAsync(Checked); + } + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TableBodyTemplate.cs b/src/ShellUI.Templates/Templates/TableBodyTemplate.cs new file mode 100644 index 0000000..2810fc6 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TableBodyTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TableBodyTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "table-body", + DisplayName = "Table Body", + Description = "Table body wrapper component", + Category = ComponentCategory.DataDisplay, + FilePath = "TableBody.razor", + Version = "0.1.0", + Tags = new List { "table", "body", "tbody" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TableCellTemplate.cs b/src/ShellUI.Templates/Templates/TableCellTemplate.cs new file mode 100644 index 0000000..9affd19 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TableCellTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TableCellTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "table-cell", + DisplayName = "Table Cell", + Description = "Table data cell component", + Category = ComponentCategory.DataDisplay, + FilePath = "TableCell.razor", + Version = "0.1.0", + Tags = new List { "table", "cell", "td" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TableHeadTemplate.cs b/src/ShellUI.Templates/Templates/TableHeadTemplate.cs new file mode 100644 index 0000000..1d0af95 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TableHeadTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TableHeadTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "table-head", + DisplayName = "Table Head", + Description = "Table header cell component", + Category = ComponentCategory.DataDisplay, + FilePath = "TableHead.razor", + Version = "0.1.0", + Tags = new List { "table", "header", "th" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TableHeaderTemplate.cs b/src/ShellUI.Templates/Templates/TableHeaderTemplate.cs new file mode 100644 index 0000000..31cdb56 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TableHeaderTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TableHeaderTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "table-header", + DisplayName = "Table Header", + Description = "Table header wrapper component", + Category = ComponentCategory.DataDisplay, + FilePath = "TableHeader.razor", + Version = "0.1.0", + Tags = new List { "table", "header", "thead" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TableRowTemplate.cs b/src/ShellUI.Templates/Templates/TableRowTemplate.cs new file mode 100644 index 0000000..1c293a8 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TableRowTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TableRowTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "table-row", + DisplayName = "Table Row", + Description = "Table row component", + Category = ComponentCategory.DataDisplay, + FilePath = "TableRow.razor", + Version = "0.1.0", + Tags = new List { "table", "row", "tr" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TableTemplate.cs b/src/ShellUI.Templates/Templates/TableTemplate.cs new file mode 100644 index 0000000..dade5f5 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TableTemplate.cs @@ -0,0 +1,38 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TableTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "table", + DisplayName = "Table", + Description = "Data table component", + Category = ComponentCategory.DataDisplay, + FilePath = "Table.razor", + Version = "0.1.0", + Tags = new List { "table", "data", "grid", "display" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + @ChildContent +
    +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TabsTemplate.cs b/src/ShellUI.Templates/Templates/TabsTemplate.cs new file mode 100644 index 0000000..5f2535e --- /dev/null +++ b/src/ShellUI.Templates/Templates/TabsTemplate.cs @@ -0,0 +1,44 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class TabsTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "tabs", + DisplayName = "Tabs", + Description = "Tabbed interface for organizing content", + Category = ComponentCategory.Layout, + Version = "0.1.0", + FilePath = "Tabs.razor", + Dependencies = new List() + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    +
    + @TabHeaders +
    +
    + @TabContent +
    +
    + +@code { + [Parameter] + public RenderFragment? TabHeaders { get; set; } + + [Parameter] + public RenderFragment? TabContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TimePickerTemplate.cs b/src/ShellUI.Templates/Templates/TimePickerTemplate.cs new file mode 100644 index 0000000..6d0395a --- /dev/null +++ b/src/ShellUI.Templates/Templates/TimePickerTemplate.cs @@ -0,0 +1,113 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TimePickerTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "time-picker", + DisplayName = "Time Picker", + Description = "Time picker component with hour and minute selection", + Category = ComponentCategory.Form, + FilePath = "TimePicker.razor", + Version = "0.1.0", + Tags = new List { "form", "time", "input", "clock" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + + + @if (_isOpen) + { +
    +
    +
    + + +
    +
    + + +
    +
    + +
    + } +
    + +@code { + [Parameter] + public TimeSpan? SelectedTime { get; set; } + + [Parameter] + public EventCallback SelectedTimeChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = ""Pick a time""; + + [Parameter] + public int Step { get; set; } = 15; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private string selectedHour = ""0""; + private string selectedMinute = ""0""; + + protected override void OnParametersSet() + { + if (SelectedTime.HasValue) + { + selectedHour = SelectedTime.Value.Hours.ToString(); + selectedMinute = SelectedTime.Value.Minutes.ToString(); + } + } + + private void TogglePicker() => _isOpen = !_isOpen; + + private async Task ApplyTime() + { + var hour = int.Parse(selectedHour); + var minute = int.Parse(selectedMinute); + SelectedTime = new TimeSpan(hour, minute, 0); + _isOpen = false; + await SelectedTimeChanged.InvokeAsync(SelectedTime); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/ToastTemplate.cs b/src/ShellUI.Templates/Templates/ToastTemplate.cs new file mode 100644 index 0000000..fffff2f --- /dev/null +++ b/src/ShellUI.Templates/Templates/ToastTemplate.cs @@ -0,0 +1,83 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class ToastTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "toast", + DisplayName = "Toast", + Description = "Toast notification component", + Category = ComponentCategory.Feedback, + FilePath = "Toast.razor", + Version = "0.1.0", + Variants = new List { "default", "destructive", "success" }, + Tags = new List { "notification", "toast", "feedback", "alert" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@if (IsVisible) +{ +
    +
    +
    + @if (!string.IsNullOrEmpty(Title)) + { +
    @Title
    + } + @if (!string.IsNullOrEmpty(Description)) + { +
    @Description
    + } + @ChildContent +
    + +
    +
    +} + +@code { + [Parameter] + public bool IsVisible { get; set; } + + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Description { get; set; } + + [Parameter] + public string Variant { get; set; } = ""default""; + + [Parameter] + public string Position { get; set; } = ""bottom-right""; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + IsVisible = false; + await IsVisibleChanged.InvokeAsync(IsVisible); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/ToggleTemplate.cs b/src/ShellUI.Templates/Templates/ToggleTemplate.cs new file mode 100644 index 0000000..ced4b74 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ToggleTemplate.cs @@ -0,0 +1,62 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class ToggleTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "toggle", + DisplayName = "Toggle", + Description = "Toggle button with pressed state", + Category = ComponentCategory.Form, + FilePath = "Toggle.razor", + Version = "0.1.0", + Variants = new List { "default", "outline" }, + Tags = new List { "form", "toggle", "button" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] + public bool Pressed { get; set; } + + [Parameter] + public EventCallback PressedChanged { get; set; } + + [Parameter] + public string Variant { get; set; } = ""default""; + + [Parameter] + public string Size { get; set; } = ""default""; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + Pressed = !Pressed; + await PressedChanged.InvokeAsync(Pressed); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TooltipTemplate.cs b/src/ShellUI.Templates/Templates/TooltipTemplate.cs new file mode 100644 index 0000000..157c741 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TooltipTemplate.cs @@ -0,0 +1,57 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class TooltipTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "tooltip", + DisplayName = "Tooltip", + Description = "Hover tooltip component", + Category = ComponentCategory.Feedback, + FilePath = "Tooltip.razor", + Version = "0.1.0", + Tags = new List { "tooltip", "hover", "feedback", "popover" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @Trigger + + @if (_isVisible) + { +
    + @Content +
    + } +
    + +@code { + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? Content { get; set; } + + [Parameter] + public string Placement { get; set; } = ""top""; + + [Parameter] + public string ClassName { get; set; } = """"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isVisible; + + private void Show() => _isVisible = true; + private void Hide() => _isVisible = false; +} +"; +} + From 80e94fb35fd1f748d38a5db7b85a8aa7e0002f4c Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 01:28:13 +0200 Subject: [PATCH 33/45] feat: Enhance app.css with new utility classes, improved spacing, and additional color variables for better styling options and responsiveness --- NET9/BlazorInteractiveServer/shellui.json | 210 ++++++ NET9/BlazorInteractiveServer/wwwroot/app.css | 730 +++++++++++++++++-- 2 files changed, 863 insertions(+), 77 deletions(-) diff --git a/NET9/BlazorInteractiveServer/shellui.json b/NET9/BlazorInteractiveServer/shellui.json index 494cd48..ef941db 100644 --- a/NET9/BlazorInteractiveServer/shellui.json +++ b/NET9/BlazorInteractiveServer/shellui.json @@ -56,6 +56,216 @@ "Version": "0.1.0", "InstalledAt": "2025-10-13T22:40:14.1393921Z", "IsCustomized": false + }, + { + "Name": "skeleton", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.0501179Z", + "IsCustomized": false + }, + { + "Name": "progress", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.1607294Z", + "IsCustomized": false + }, + { + "Name": "separator", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.1782159Z", + "IsCustomized": false + }, + { + "Name": "select", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.1946471Z", + "IsCustomized": false + }, + { + "Name": "checkbox", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.2102138Z", + "IsCustomized": false + }, + { + "Name": "switch", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.2262595Z", + "IsCustomized": false + }, + { + "Name": "tabs", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.2616461Z", + "IsCustomized": false + }, + { + "Name": "navbar", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.2897841Z", + "IsCustomized": false + }, + { + "Name": "sidebar", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.3112969Z", + "IsCustomized": false + }, + { + "Name": "dropdown", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:15:44.3243223Z", + "IsCustomized": false + }, + { + "Name": "radio-group", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.4625721Z", + "IsCustomized": false + }, + { + "Name": "radio-group-item", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.5314243Z", + "IsCustomized": false + }, + { + "Name": "slider", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.5377327Z", + "IsCustomized": false + }, + { + "Name": "toggle", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.5486562Z", + "IsCustomized": false + }, + { + "Name": "accordion", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.585104Z", + "IsCustomized": false + }, + { + "Name": "accordion-item", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.5960899Z", + "IsCustomized": false + }, + { + "Name": "breadcrumb", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.6152002Z", + "IsCustomized": false + }, + { + "Name": "breadcrumb-item", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.633287Z", + "IsCustomized": false + }, + { + "Name": "toast", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.6416099Z", + "IsCustomized": false + }, + { + "Name": "tooltip", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.6527488Z", + "IsCustomized": false + }, + { + "Name": "popover", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.6645514Z", + "IsCustomized": false + }, + { + "Name": "avatar", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.684572Z", + "IsCustomized": false + }, + { + "Name": "table", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.7009628Z", + "IsCustomized": false + }, + { + "Name": "table-header", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.7204408Z", + "IsCustomized": false + }, + { + "Name": "table-body", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.7406296Z", + "IsCustomized": false + }, + { + "Name": "table-row", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.7665971Z", + "IsCustomized": false + }, + { + "Name": "table-head", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.7838252Z", + "IsCustomized": false + }, + { + "Name": "table-cell", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T21:56:43.8026543Z", + "IsCustomized": false + }, + { + "Name": "form", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T22:41:11.8831646Z", + "IsCustomized": false + }, + { + "Name": "input-otp", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T22:41:12.3651669Z", + "IsCustomized": false + }, + { + "Name": "combobox", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T22:41:12.4045265Z", + "IsCustomized": false + }, + { + "Name": "date-picker", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T22:41:12.4413046Z", + "IsCustomized": false + }, + { + "Name": "time-picker", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T22:41:12.5235144Z", + "IsCustomized": false + }, + { + "Name": "pagination", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T22:41:12.5807953Z", + "IsCustomized": false + }, + { + "Name": "date-range-picker", + "Version": "0.1.0", + "InstalledAt": "2025-10-15T23:17:17.3769239Z", + "IsCustomized": false } ], "ProjectType": 1 diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index 57ae223..0acdbda 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -2,12 +2,16 @@ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @layer base { *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; --tw-rotate-x: rotateX(0); --tw-rotate-y: rotateY(0); --tw-rotate-z: rotateZ(0); --tw-skew-x: skewX(0); --tw-skew-y: skewY(0); --tw-space-y-reverse: 0; + --tw-space-x-reverse: 0; --tw-border-style: solid; --tw-leading: initial; --tw-font-weight: initial; @@ -49,10 +53,7 @@ --tw-backdrop-opacity: initial; --tw-backdrop-saturate: initial; --tw-backdrop-sepia: initial; - --tw-space-x-reverse: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-translate-z: 0; + --tw-duration: initial; } } } @@ -67,10 +68,13 @@ --color-yellow-500: oklch(79.5% 0.184 86.047); --color-yellow-600: oklch(68.1% 0.162 75.834); --color-yellow-700: oklch(55.4% 0.135 66.442); + --color-green-50: oklch(98.2% 0.018 155.826); + --color-green-100: oklch(96.2% 0.044 156.743); --color-green-400: oklch(79.2% 0.209 151.711); --color-green-500: oklch(72.3% 0.219 149.579); --color-green-600: oklch(62.7% 0.194 149.214); --color-green-700: oklch(52.7% 0.154 150.069); + --color-green-900: oklch(39.3% 0.095 152.535); --color-blue-400: oklch(70.7% 0.165 254.624); --color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-600: oklch(54.6% 0.245 262.881); @@ -78,6 +82,7 @@ --color-black: #000; --color-white: #fff; --spacing: 0.25rem; + --container-xs: 20rem; --container-md: 28rem; --container-lg: 32rem; --container-4xl: 56rem; @@ -97,12 +102,13 @@ --text-3xl--line-height: calc(2.25 / 1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5 / 2.25); + --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-tight: -0.025em; - --radius-xl: 0.75rem; --animate-spin: spin 1s linear infinite; + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; --blur-sm: 8px; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -253,8 +259,11 @@ } } @layer utilities { - .collapse { - visibility: collapse; + .pointer-events-auto { + pointer-events: auto; + } + .pointer-events-none { + pointer-events: none; } .absolute { position: absolute; @@ -268,38 +277,53 @@ .static { position: static; } + .sticky { + position: sticky; + } .inset-0 { inset: calc(var(--spacing) * 0); } - .inset-y-0 { - inset-block: calc(var(--spacing) * 0); - } - .inset-y-1 { - inset-block: calc(var(--spacing) * 1); - } - .inset-y-2\.5 { - inset-block: calc(var(--spacing) * 2.5); - } - .end-0 { - inset-inline-end: calc(var(--spacing) * 0); - } .top-0 { top: calc(var(--spacing) * 0); } + .top-1\/2 { + top: calc(1/2 * 100%); + } + .top-2 { + top: calc(var(--spacing) * 2); + } .top-4 { top: calc(var(--spacing) * 4); } + .top-full { + top: 100%; + } .right-0 { right: calc(var(--spacing) * 0); } + .right-2 { + right: calc(var(--spacing) * 2); + } .right-4 { right: calc(var(--spacing) * 4); } + .right-full { + right: 100%; + } + .bottom-0 { + bottom: calc(var(--spacing) * 0); + } + .bottom-full { + bottom: 100%; + } .left-0 { left: calc(var(--spacing) * 0); } - .z-20 { - z-index: 20; + .left-1\/2 { + left: calc(1/2 * 100%); + } + .left-full { + left: 100%; } .z-40 { z-index: 40; @@ -325,11 +349,17 @@ max-width: 96rem; } } + .mx-2 { + margin-inline: calc(var(--spacing) * 2); + } .mx-auto { margin-inline: auto; } - .ms-auto { - margin-inline-start: auto; + .my-4 { + margin-block: calc(var(--spacing) * 4); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); } .mt-2 { margin-top: calc(var(--spacing) * 2); @@ -343,15 +373,21 @@ .mt-12 { margin-top: calc(var(--spacing) * 12); } + .mr-0 { + margin-right: calc(var(--spacing) * 0); + } + .mr-1 { + margin-right: calc(var(--spacing) * 1); + } + .mr-2 { + margin-right: calc(var(--spacing) * 2); + } .mr-3 { margin-right: calc(var(--spacing) * 3); } .mb-1 { margin-bottom: calc(var(--spacing) * 1); } - .mb-1\.5 { - margin-bottom: calc(var(--spacing) * 1.5); - } .mb-2 { margin-bottom: calc(var(--spacing) * 2); } @@ -361,12 +397,24 @@ .mb-4 { margin-bottom: calc(var(--spacing) * 4); } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } .mb-8 { margin-bottom: calc(var(--spacing) * 8); } .-ml-1 { margin-left: calc(var(--spacing) * -1); } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .ml-64 { + margin-left: calc(var(--spacing) * 64); + } + .block { + display: block; + } .flex { display: flex; } @@ -376,9 +424,36 @@ .hidden { display: none; } + .inline-block { + display: inline-block; + } .inline-flex { display: inline-flex; } + .table { + display: table; + } + .table-cell { + display: table-cell; + } + .table-row { + display: table-row; + } + .aspect-square { + aspect-ratio: 1 / 1; + } + .h-1\/2 { + height: calc(1/2 * 100%); + } + .h-2 { + height: calc(var(--spacing) * 2); + } + .h-2\.5 { + height: calc(var(--spacing) * 2.5); + } + .h-3\.5 { + height: calc(var(--spacing) * 3.5); + } .h-4 { height: calc(var(--spacing) * 4); } @@ -400,15 +475,60 @@ .h-11 { height: calc(var(--spacing) * 11); } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-14 { + height: calc(var(--spacing) * 14); + } + .h-16 { + height: calc(var(--spacing) * 16); + } + .h-20 { + height: calc(var(--spacing) * 20); + } + .h-64 { + height: calc(var(--spacing) * 64); + } + .h-96 { + height: calc(var(--spacing) * 96); + } + .h-\[1px\] { + height: 1px; + } .h-full { height: 100%; } + .h-screen { + height: 100vh; + } + .max-h-60 { + max-height: calc(var(--spacing) * 60); + } + .max-h-screen { + max-height: 100vh; + } .min-h-\[80px\] { min-height: 80px; } .min-h-screen { min-height: 100vh; } + .w-1\/2 { + width: calc(1/2 * 100%); + } + .w-2\.5 { + width: calc(var(--spacing) * 2.5); + } + .w-2\/3 { + width: calc(2/3 * 100%); + } + .w-3\.5 { + width: calc(var(--spacing) * 3.5); + } + .w-3\/4 { + width: calc(3/4 * 100%); + } .w-4 { width: calc(var(--spacing) * 4); } @@ -430,12 +550,27 @@ .w-11 { width: calc(var(--spacing) * 11); } - .w-full { - width: 100%; + .w-12 { + width: calc(var(--spacing) * 12); + } + .w-16 { + width: calc(var(--spacing) * 16); } - .w-px { + .w-56 { + width: calc(var(--spacing) * 56); + } + .w-64 { + width: calc(var(--spacing) * 64); + } + .w-72 { + width: calc(var(--spacing) * 72); + } + .w-\[1px\] { width: 1px; } + .w-full { + width: 100%; + } .max-w-4xl { max-width: var(--container-4xl); } @@ -445,48 +580,101 @@ .max-w-md { max-width: var(--container-md); } + .max-w-xs { + max-width: var(--container-xs); + } .flex-1 { flex: 1; } .flex-shrink-0 { flex-shrink: 0; } + .shrink-0 { + flex-shrink: 0; + } + .caption-bottom { + caption-side: bottom; + } + .origin-top-right { + transform-origin: top right; + } + .-translate-x-1\/2 { + --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: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-x-0 { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-x-5 { + --tw-translate-x: calc(var(--spacing) * 5); + 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); + } + .rotate-180 { + rotate: 180deg; + } .transform { transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y); } + .animate-pulse { + animation: var(--animate-pulse); + } .animate-spin { animation: var(--animate-spin); } + .cursor-default { + cursor: default; + } + .cursor-not-allowed { + cursor: not-allowed; + } + .cursor-pointer { + cursor: pointer; + } + .touch-none { + touch-action: none; + } + .appearance-none { + appearance: none; + } + .grid-cols-7 { + grid-template-columns: repeat(7, minmax(0, 1fr)); + } .flex-col { flex-direction: column; } .flex-col-reverse { flex-direction: column-reverse; } - .flex-row { - flex-direction: row; - } .flex-wrap { flex-wrap: wrap; } .items-center { align-items: center; } - .items-end { - align-items: flex-end; - } .justify-between { justify-content: space-between; } .justify-center { justify-content: center; } + .gap-1 { + gap: calc(var(--spacing) * 1); + } + .gap-1\.5 { + gap: calc(var(--spacing) * 1.5); + } .gap-2 { gap: calc(var(--spacing) * 2); } - .gap-2\.5 { - gap: calc(var(--spacing) * 2.5); - } .gap-3 { gap: calc(var(--spacing) * 3); } @@ -503,6 +691,20 @@ margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-2 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-3 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-4 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -510,6 +712,13 @@ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-6 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-8 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -517,6 +726,40 @@ margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); } } + .space-x-1 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-2 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-4 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-6 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse))); + } + } + .overflow-auto { + overflow: auto; + } + .overflow-hidden { + overflow: hidden; + } .overflow-y-auto { overflow-y: auto; } @@ -535,21 +778,22 @@ .rounded-sm { border-radius: calc(var(--radius) - 4px); } - .rounded-xl { - border-radius: var(--radius-xl); - } .border { border-style: var(--tw-border-style); border-width: 1px; } - .border-s { - border-inline-start-style: var(--tw-border-style); - border-inline-start-width: 1px; + .border-2 { + border-style: var(--tw-border-style); + border-width: 2px; } .border-t { border-top-style: var(--tw-border-style); border-top-width: 1px; } + .border-r { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } .border-b { border-bottom-style: var(--tw-border-style); border-bottom-width: 1px; @@ -563,9 +807,15 @@ .border-border { border-color: var(--border); } + .border-destructive { + border-color: var(--destructive); + } .border-destructive\/50 { border-color: color-mix(in oklab, var(--destructive) 50%, transparent); } + .border-green-500 { + border-color: var(--color-green-500); + } .border-green-500\/50 { border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -575,6 +825,12 @@ .border-input { border-color: var(--input); } + .border-primary { + border-color: var(--primary); + } + .border-ring { + border-color: var(--ring); + } .border-transparent { border-color: transparent; } @@ -590,6 +846,9 @@ .bg-background { background-color: var(--background); } + .bg-background\/95 { + background-color: color-mix(in oklab, var(--background) 95%, transparent); + } .bg-black\/50 { background-color: color-mix(in srgb, #000 50%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -599,39 +858,66 @@ .bg-blue-500 { background-color: var(--color-blue-500); } + .bg-border { + background-color: var(--border); + } .bg-card { background-color: var(--card); } .bg-destructive { background-color: var(--destructive); } + .bg-green-50 { + background-color: var(--color-green-50); + } .bg-green-500 { background-color: var(--color-green-500); } + .bg-input { + background-color: var(--input); + } .bg-muted { background-color: var(--muted); } .bg-muted\/50 { background-color: color-mix(in oklab, var(--muted) 50%, transparent); } + .bg-popover { + background-color: var(--popover); + } .bg-primary { background-color: var(--primary); } + .bg-primary\/20 { + background-color: color-mix(in oklab, var(--primary) 20%, transparent); + } .bg-secondary { background-color: var(--secondary); } + .bg-transparent { + background-color: transparent; + } .bg-yellow-500 { background-color: var(--color-yellow-500); } + .fill-primary-foreground { + fill: var(--primary-foreground); + } + .object-cover { + object-fit: cover; + } .p-0\.5 { padding: calc(var(--spacing) * 0.5); } - .p-1\.5 { - padding: calc(var(--spacing) * 1.5); + .p-1 { + padding: calc(var(--spacing) * 1); } .p-2 { padding: calc(var(--spacing) * 2); } + .p-3 { + padding: calc(var(--spacing) * 3); + } .p-4 { padding: calc(var(--spacing) * 4); } @@ -650,6 +936,9 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } + .px-5 { + padding-inline: calc(var(--spacing) * 5); + } .px-8 { padding-inline: calc(var(--spacing) * 8); } @@ -659,6 +948,9 @@ .py-1 { padding-block: calc(var(--spacing) * 1); } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } .py-2 { padding-block: calc(var(--spacing) * 2); } @@ -671,13 +963,29 @@ .pt-0 { padding-top: calc(var(--spacing) * 0); } + .pr-8 { + padding-right: calc(var(--spacing) * 8); + } + .pb-4 { + padding-bottom: calc(var(--spacing) * 4); + } .text-center { text-align: center; } + .text-left { + text-align: left; + } + .align-middle { + vertical-align: middle; + } .text-2xl { font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); @@ -686,6 +994,10 @@ font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } .text-xs { font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); @@ -702,6 +1014,10 @@ --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } + .font-normal { + --tw-font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-normal); + } .font-semibold { --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); @@ -710,6 +1026,9 @@ --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); } + .break-words { + overflow-wrap: break-word; + } .whitespace-nowrap { white-space: nowrap; } @@ -734,9 +1053,15 @@ .text-green-700 { color: var(--color-green-700); } + .text-green-900 { + color: var(--color-green-900); + } .text-muted-foreground { color: var(--muted-foreground); } + .text-popover-foreground { + color: var(--popover-foreground); + } .text-primary { color: var(--primary); } @@ -758,6 +1083,9 @@ .opacity-25 { opacity: 25%; } + .opacity-50 { + opacity: 50%; + } .opacity-70 { opacity: 70%; } @@ -767,14 +1095,37 @@ .opacity-90 { opacity: 90%; } + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-lg { --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-sm { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .ring-0 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .ring-2 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .ring-ring { + --tw-ring-color: var(--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); + } .ring-offset-background { --tw-ring-offset-color: var(--background); } @@ -786,13 +1137,18 @@ --tw-blur: blur(8px); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } + .backdrop-blur { + --tw-backdrop-blur: blur(8px); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } .backdrop-blur-sm { --tw-backdrop-blur: blur(var(--blur-sm)); -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } - .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter; + .transition-all { + transition-property: all; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } @@ -811,6 +1167,18 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + .outline-none { + --tw-outline-style: none; + outline-style: none; + } + .select-none { + -webkit-user-select: none; + user-select: none; + } .peer-disabled\:cursor-not-allowed { &:is(:where(.peer):disabled ~ *) { cursor: not-allowed; @@ -884,6 +1252,20 @@ } } } + .hover\:bg-muted { + &:hover { + @media (hover: hover) { + background-color: var(--muted); + } + } + } + .hover\:bg-muted\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--muted) 50%, transparent); + } + } + } .hover\:bg-primary\/80 { &:hover { @media (hover: hover) { @@ -926,6 +1308,20 @@ } } } + .hover\:text-foreground\/80 { + &:hover { + @media (hover: hover) { + color: color-mix(in oklab, var(--foreground) 80%, transparent); + } + } + } + .hover\:text-muted-foreground { + &:hover { + @media (hover: hover) { + color: var(--muted-foreground); + } + } + } .hover\:underline { &:hover { @media (hover: hover) { @@ -940,6 +1336,17 @@ } } } + .focus\:border-ring { + &:focus { + border-color: var(--ring); + } + } + .focus\:ring-1 { + &:focus { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } .focus\:ring-2 { &:focus { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); @@ -986,6 +1393,11 @@ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); } } + .focus-visible\:ring-offset-background { + &:focus-visible { + --tw-ring-offset-color: var(--background); + } + } .focus-visible\:outline-none { &:focus-visible { --tw-outline-style: none; @@ -1007,9 +1419,14 @@ opacity: 50%; } } - .max-md\:hidden { - @media (width < 48rem) { - display: none; + .data-\[state\=selected\]\:bg-muted { + &[data-state="selected"] { + background-color: var(--muted); + } + } + .supports-\[backdrop-filter\]\:bg-background\/60 { + @supports (backdrop-filter: var(--tw)) { + background-color: color-mix(in oklab, var(--background) 60%, transparent); } } .sm\:inline-flex { @@ -1022,6 +1439,11 @@ width: auto; } } + .sm\:flex-col { + @media (width >= 40rem) { + flex-direction: column; + } + } .sm\:flex-row { @media (width >= 40rem) { flex-direction: row; @@ -1032,6 +1454,11 @@ justify-content: flex-end; } } + .sm\:gap-2\.5 { + @media (width >= 40rem) { + gap: calc(var(--spacing) * 2.5); + } + } .sm\:space-x-2 { @media (width >= 40rem) { :where(& > :not(:last-child)) { @@ -1069,14 +1496,14 @@ line-height: var(--tw-leading, var(--text-xl--line-height)); } } - .md\:hidden { + .md\:inline-flex { @media (width >= 48rem) { - display: none; + display: inline-flex; } } - .md\:inline-flex { + .md\:max-w-\[420px\] { @media (width >= 48rem) { - display: inline-flex; + max-width: 420px; } } .md\:grid-cols-2 { @@ -1095,11 +1522,6 @@ line-height: var(--tw-leading, var(--text-4xl--line-height)); } } - .xl\:start-4 { - @media (width >= 80rem) { - inset-inline-start: calc(var(--spacing) * 4); - } - } .dark\:border-blue-500 { @media (prefers-color-scheme: dark) { border-color: var(--color-blue-500); @@ -1120,11 +1542,24 @@ border-color: var(--color-yellow-500); } } + .dark\:bg-green-900\/20 { + @media (prefers-color-scheme: dark) { + background-color: color-mix(in srgb, oklch(39.3% 0.095 152.535) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-green-900) 20%, transparent); + } + } + } .dark\:text-blue-400 { @media (prefers-color-scheme: dark) { color: var(--color-blue-400); } } + .dark\:text-green-100 { + @media (prefers-color-scheme: dark) { + color: var(--color-green-100); + } + } .dark\:text-green-400 { @media (prefers-color-scheme: dark) { color: var(--color-green-400); @@ -1135,6 +1570,138 @@ color: var(--color-yellow-400); } } + .\[\&_tr\]\:border-b { + & tr { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + } + .\[\&_tr\:last-child\]\:border-0 { + & tr:last-child { + border-style: var(--tw-border-style); + border-width: 0px; + } + } + .\[\&\:\:-moz-range-thumb\]\:h-5 { + &::-moz-range-thumb { + height: calc(var(--spacing) * 5); + } + } + .\[\&\:\:-moz-range-thumb\]\:w-5 { + &::-moz-range-thumb { + width: calc(var(--spacing) * 5); + } + } + .\[\&\:\:-moz-range-thumb\]\:rounded-full { + &::-moz-range-thumb { + border-radius: calc(infinity * 1px); + } + } + .\[\&\:\:-moz-range-thumb\]\:bg-primary { + &::-moz-range-thumb { + background-color: var(--primary); + } + } + .\[\&\:\:-moz-range-thumb\]\:ring-offset-background { + &::-moz-range-thumb { + --tw-ring-offset-color: var(--background); + } + } + .\[\&\:\:-moz-range-thumb\]\:transition-colors { + &::-moz-range-thumb { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + } + .focus-visible\:\[\&\:\:-moz-range-thumb\]\:ring-2 { + &:focus-visible { + &::-moz-range-thumb { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + .focus-visible\:\[\&\:\:-moz-range-thumb\]\:ring-ring { + &:focus-visible { + &::-moz-range-thumb { + --tw-ring-color: var(--ring); + } + } + } + .focus-visible\:\[\&\:\:-moz-range-thumb\]\:ring-offset-2 { + &:focus-visible { + &::-moz-range-thumb { + --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); + } + } + } + .\[\&\:\:-webkit-slider-thumb\]\:h-5 { + &::-webkit-slider-thumb { + height: calc(var(--spacing) * 5); + } + } + .\[\&\:\:-webkit-slider-thumb\]\:w-5 { + &::-webkit-slider-thumb { + width: calc(var(--spacing) * 5); + } + } + .\[\&\:\:-webkit-slider-thumb\]\:appearance-none { + &::-webkit-slider-thumb { + appearance: none; + } + } + .\[\&\:\:-webkit-slider-thumb\]\:rounded-full { + &::-webkit-slider-thumb { + border-radius: calc(infinity * 1px); + } + } + .\[\&\:\:-webkit-slider-thumb\]\:bg-primary { + &::-webkit-slider-thumb { + background-color: var(--primary); + } + } + .\[\&\:\:-webkit-slider-thumb\]\:ring-offset-background { + &::-webkit-slider-thumb { + --tw-ring-offset-color: var(--background); + } + } + .\[\&\:\:-webkit-slider-thumb\]\:transition-colors { + &::-webkit-slider-thumb { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + } + .focus-visible\:\[\&\:\:-webkit-slider-thumb\]\:ring-2 { + &:focus-visible { + &::-webkit-slider-thumb { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + .focus-visible\:\[\&\:\:-webkit-slider-thumb\]\:ring-ring { + &:focus-visible { + &::-webkit-slider-thumb { + --tw-ring-color: var(--ring); + } + } + } + .focus-visible\:\[\&\:\:-webkit-slider-thumb\]\:ring-offset-2 { + &:focus-visible { + &::-webkit-slider-thumb { + --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); + } + } + } + .\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0 { + &:has([role=checkbox]) { + padding-right: calc(var(--spacing) * 0); + } + } .\[\&\>svg\]\:absolute { &>svg { position: absolute; @@ -1239,6 +1806,21 @@ color: var(--foreground); } } +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-rotate-x { syntax: "*"; inherits: false; @@ -1269,6 +1851,11 @@ inherits: false; initial-value: 0; } +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-border-style { syntax: "*"; inherits: false; @@ -1445,28 +2032,17 @@ syntax: "*"; inherits: false; } -@property --tw-space-x-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-x { +@property --tw-duration { syntax: "*"; inherits: false; - initial-value: 0; -} -@property --tw-translate-y { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-z { - syntax: "*"; - inherits: false; - initial-value: 0; } @keyframes spin { to { transform: rotate(360deg); } } +@keyframes pulse { + 50% { + opacity: 0.5; + } +} From 7f5c47bfc250469fac4ce31a7ee221fef95f4fe3 Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 01:29:18 +0200 Subject: [PATCH 34/45] feat: Add comprehensive showcase of new UI components on Home page, including Skeleton, Progress, Select, Checkbox, Switch, Dropdown, Navbar, Sidebar, Tabs, and more, enhancing user interaction and demonstrating component capabilities --- .../Components/Pages/Home.razor | 676 +++++++++++++++++- 1 file changed, 675 insertions(+), 1 deletion(-) diff --git a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor index 44d77c5..7c9558f 100644 --- a/NET9/BlazorInteractiveServer/Components/Pages/Home.razor +++ b/NET9/BlazorInteractiveServer/Components/Pages/Home.razor @@ -142,6 +142,603 @@

    + +
    +

    New Components

    + + +
    +

    Skeleton

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

    Progress

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

    Separator

    +
    +
    +

    Section 1

    + +

    Section 2

    +
    +
    +
    Column 1
    + +
    Column 2
    +
    +
    +
    + + +
    +

    Select

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

    Selected: @selectedFruit

    + } +
    +
    + + +
    +

    Checkbox & Switch

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

    Dropdown

    + + + Open Menu + + + + + + + + + +
    + + +
    +

    Navbar

    +
    + +
    + ShellUI + + +
    +
    +
    +

    This navbar sticks to the top with backdrop blur effect! Perfect for app headers.

    +

    Try scrolling the main page to see it in action.

    +
    +
    +
    + + +
    +

    Sidebar

    +
    + +
    + + + +
    +

    Main Content Area

    +

    + Toggle the sidebar to see the smooth slide-in animation! Perfect for app navigation menus. +

    +
    +
    +
    +
    + + +
    +

    Tabs

    + + + + + + + + @if (activeTab == "account") + { +
    +

    Manage your account settings and preferences.

    +
    + + +
    +
    + + +
    + +
    + } + else if (activeTab == "password") + { +
    +

    Change your password here. After saving, you'll be logged out.

    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + } + else + { +
    +

    Configure how you receive notifications.

    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + } +
    +
    +
    + + +

    New Components

    + + +
    +

    Radio Group

    + + + Default + Comfortable + Compact + + + @if (!string.IsNullOrEmpty(selectedOption)) + { +

    Selected: @selectedOption

    + } +
    + + +
    +

    Slider

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

    Toggle

    +
    + + + + + + Toggle Me + + + Outline + Small +
    +

    Pressed: @(togglePressed ? "Yes" : "No")

    +
    + + +
    +

    Accordion

    + + + +

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

    +
    + +

    Yes. It comes with default styles that matches the other components aesthetic.

    +
    + +

    Yes. It's animated by default using Tailwind CSS transitions.

    +
    +
    +
    +
    + + +
    +

    Breadcrumb

    + + Home + Components + UI + Breadcrumb + +
    + + +
    +

    Toast

    +
    + + + +
    +
    + + +
    +

    Tooltip

    +
    + + + + + +

    This is a tooltip on top!

    +
    +
    + + + + + +

    This is a tooltip on bottom!

    +
    +
    + + + + + +

    Tooltip on the left

    +
    +
    +
    +
    + + +
    +

    Popover

    + + + + + +
    +

    Dimensions

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

    Avatar

    +
    + + + + +
    +
    + + +
    +

    Table

    + + + + Name + Email + Role + Status + + + + + Shewart Shepherd + shep@shell-tech.dev + Founder & CEO + Active + + + Shewart Shepherd + +263780000000 + Lead Developer + Active + + + Shell Technologies + contact@shell-tech.dev + Organization + Verified + + +
    +
    + + +
    +

    Input OTP

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + @if (!string.IsNullOrEmpty(otp1) || !string.IsNullOrEmpty(otp2) || !string.IsNullOrEmpty(otp3)) + { +
    +

    You have entered OTP:

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

    Grouped by 3: @otp1

    + } + @if (!string.IsNullOrEmpty(otp2)) + { +

    Grouped by 2: @otp2

    + } + @if (!string.IsNullOrEmpty(otp3)) + { +

    No grouping: @otp3

    + } +
    + } +
    +
    + + +
    +

    Form

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + @if (formSubmitted) + { + + Form submitted successfully! + + } +
    + + +
    +

    Combobox

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

    Selected: @selectedTech

    + } +
    +
    + + + @if (!string.IsNullOrEmpty(selectedFramework)) + { +

    Selected: @selectedFramework

    + } +
    +
    +
    + + +
    +

    Date Picker

    +
    +
    + + + @if (selectedDate.HasValue) + { +

    Selected: @selectedDate.Value.ToString("MMMM dd, yyyy")

    + } +
    +
    + + + @if (selectedDate2.HasValue) + { +

    Selected: @selectedDate2.Value.ToString("MMMM dd, yyyy")

    + } +
    +
    + + + @if (rangeStartDate.HasValue || rangeEndDate.HasValue) + { +

    + Range: @(rangeStartDate?.ToString("MMM dd") ?? "...") - @(rangeEndDate?.ToString("MMM dd, yyyy") ?? "...") +

    + } +
    +
    +
    + + +
    +

    Time Picker

    +
    + + @if (selectedTime.HasValue) + { +

    Selected: @selectedTime.Value.ToString(@"hh\:mm")

    + } +
    +
    + + +
    +

    Pagination

    +
    +
    +

    Showing page @currentPage of @totalPages

    +
    + +
    +
    +
    + + + + + + @@ -158,11 +755,70 @@ private int clickCount = 0; private bool isLoading = false; private bool isDialogOpen = false; + private bool isDropdownOpen = false; + private bool isSidebarOpen = false; // Form inputs private string userName = ""; private string userEmail = ""; private string userMessage = ""; + private string selectedFruit = ""; + + // Checkbox & Switch states + private bool acceptTerms = false; + private bool notifications = true; + + // Tabs demo state + private string activeTab = "account"; + private string tabUsername = ""; + private string tabEmail = ""; + + // Notification preferences + private bool emailNotifications = true; + private bool pushNotifications = false; + private bool smsNotifications = false; + + // New component states + private RadioGroup? radioGroup; + private string selectedOption = ""; + private double sliderValue = 50; + private double temperature = 20; + private bool togglePressed = false; + private bool togglePressed2 = false; + private bool togglePressed3 = false; + private Accordion? accordion; + private bool showToast = false; + private bool showDestructiveToast = false; + private bool showSuccessToast = false; + private bool isPopoverOpen = false; + + // New component states + private string formName = ""; + private string formEmail = ""; + private string formPhone = ""; + private string otp1 = ""; + private string otp2 = ""; + private string otp3 = ""; + private bool formSubmitted = false; + private string selectedTech = ""; + private string selectedFramework = ""; + private DateTime? selectedDate; + private DateTime? selectedDate2 = DateTime.Now; + private DateTime? rangeStartDate; + private DateTime? rangeEndDate; + private TimeSpan? selectedTime; + private int currentPage = 1; + private int totalPages = 10; + + private List techOptions = new() + { + "C#", "JavaScript", "TypeScript", "Python", "Java", "Go", "Rust", "F#" + }; + + private List frameworkOptions = new() + { + "Blazor", "ASP.NET Core", "React", "Vue", "Angular", "Next.js", "Svelte", "ShellUI" + }; private void HandleClick(MouseEventArgs args) { @@ -173,7 +829,7 @@ { isLoading = true; clickCount++; - await Task.Delay(2000); // Simulate async operation + await Task.Delay(2000); isLoading = false; } @@ -186,4 +842,22 @@ { isDialogOpen = false; } + + private void ToggleSidebar() + { + isSidebarOpen = !isSidebarOpen; + } + + private async Task HandleFormSubmit() + { + formSubmitted = true; + await Task.Delay(3000); + formSubmitted = false; + } + + private async Task HandlePageChange(int page) + { + currentPage = page; + await Task.CompletedTask; + } } \ No newline at end of file From 83a338dc743dc9d17095244823291f283b5ac91a Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 01:29:54 +0200 Subject: [PATCH 35/45] feat: Introduce a wide range of new UI components including Accordion, AccordionItem, Avatar, Breadcrumb, Checkbox, Collapsible, Combobox, DatePicker, DateRangePicker, Drawer, Dropdown, Form, InputOTP, Menubar, Navbar, Pagination, Popover, Progress, RadioGroup, Resizable, ScrollArea, Select, Separator, Sheet, Sidebar, Skeleton, Slider, Switch, Table, Tabs, TimePicker, Toast, Toggle, and Tooltip to enhance the ShellUI component library, improving user interface options and functionality. --- .../Components/Accordion.razor | 29 +++ .../Components/AccordionItem.razor | 54 +++++ .../Components/Avatar.razor | 51 +++++ .../Components/Breadcrumb.razor | 19 ++ .../Components/BreadcrumbItem.razor | 41 ++++ .../Components/Checkbox.razor | 44 ++++ .../Components/Collapsible.razor | 46 +++++ .../Components/Combobox.razor | 71 +++++++ .../Components/DatePicker.razor | 101 +++++++++ .../Components/DateRangePicker.razor | 195 ++++++++++++++++++ .../Components/Drawer.razor | 74 +++++++ .../Components/Dropdown.razor | 44 ++++ src/ShellUI.Components/Components/Form.razor | 30 +++ .../Components/InputOTP.razor | 146 +++++++++++++ .../Components/Menubar.razor | 17 ++ .../Components/MenubarItem.razor | 41 ++++ .../Components/Navbar.razor | 19 ++ .../Components/NavigationMenu.razor | 19 ++ .../Components/NavigationMenuItem.razor | 44 ++++ .../Components/Pagination.razor | 83 ++++++++ .../Components/Popover.razor | 63 ++++++ .../Components/Progress.razor | 20 ++ .../Components/RadioGroup.razor | 32 +++ .../Components/RadioGroupItem.razor | 52 +++++ .../Components/Resizable.razor | 20 ++ .../Components/ScrollArea.razor | 22 ++ .../Components/Select.razor | 37 ++++ .../Components/Separator.razor | 15 ++ src/ShellUI.Components/Components/Sheet.razor | 72 +++++++ .../Components/Sidebar.razor | 22 ++ .../Components/Skeleton.razor | 15 ++ .../Components/Slider.razor | 48 +++++ .../Components/Switch.razor | 39 ++++ src/ShellUI.Components/Components/Table.razor | 19 ++ .../Components/TableBody.razor | 17 ++ .../Components/TableCell.razor | 17 ++ .../Components/TableHead.razor | 17 ++ .../Components/TableHeader.razor | 17 ++ .../Components/TableRow.razor | 17 ++ src/ShellUI.Components/Components/Tabs.razor | 25 +++ .../Components/TimePicker.razor | 100 +++++++++ src/ShellUI.Components/Components/Toast.razor | 78 +++++++ .../Components/Toggle.razor | 55 +++++ .../Components/Tooltip.razor | 46 +++++ 44 files changed, 2033 insertions(+) create mode 100644 src/ShellUI.Components/Components/Accordion.razor create mode 100644 src/ShellUI.Components/Components/AccordionItem.razor create mode 100644 src/ShellUI.Components/Components/Avatar.razor create mode 100644 src/ShellUI.Components/Components/Breadcrumb.razor create mode 100644 src/ShellUI.Components/Components/BreadcrumbItem.razor create mode 100644 src/ShellUI.Components/Components/Checkbox.razor create mode 100644 src/ShellUI.Components/Components/Collapsible.razor create mode 100644 src/ShellUI.Components/Components/Combobox.razor create mode 100644 src/ShellUI.Components/Components/DatePicker.razor create mode 100644 src/ShellUI.Components/Components/DateRangePicker.razor create mode 100644 src/ShellUI.Components/Components/Drawer.razor create mode 100644 src/ShellUI.Components/Components/Dropdown.razor create mode 100644 src/ShellUI.Components/Components/Form.razor create mode 100644 src/ShellUI.Components/Components/InputOTP.razor create mode 100644 src/ShellUI.Components/Components/Menubar.razor create mode 100644 src/ShellUI.Components/Components/MenubarItem.razor create mode 100644 src/ShellUI.Components/Components/Navbar.razor create mode 100644 src/ShellUI.Components/Components/NavigationMenu.razor create mode 100644 src/ShellUI.Components/Components/NavigationMenuItem.razor create mode 100644 src/ShellUI.Components/Components/Pagination.razor create mode 100644 src/ShellUI.Components/Components/Popover.razor create mode 100644 src/ShellUI.Components/Components/Progress.razor create mode 100644 src/ShellUI.Components/Components/RadioGroup.razor create mode 100644 src/ShellUI.Components/Components/RadioGroupItem.razor create mode 100644 src/ShellUI.Components/Components/Resizable.razor create mode 100644 src/ShellUI.Components/Components/ScrollArea.razor create mode 100644 src/ShellUI.Components/Components/Select.razor create mode 100644 src/ShellUI.Components/Components/Separator.razor create mode 100644 src/ShellUI.Components/Components/Sheet.razor create mode 100644 src/ShellUI.Components/Components/Sidebar.razor create mode 100644 src/ShellUI.Components/Components/Skeleton.razor create mode 100644 src/ShellUI.Components/Components/Slider.razor create mode 100644 src/ShellUI.Components/Components/Switch.razor create mode 100644 src/ShellUI.Components/Components/Table.razor create mode 100644 src/ShellUI.Components/Components/TableBody.razor create mode 100644 src/ShellUI.Components/Components/TableCell.razor create mode 100644 src/ShellUI.Components/Components/TableHead.razor create mode 100644 src/ShellUI.Components/Components/TableHeader.razor create mode 100644 src/ShellUI.Components/Components/TableRow.razor create mode 100644 src/ShellUI.Components/Components/Tabs.razor create mode 100644 src/ShellUI.Components/Components/TimePicker.razor create mode 100644 src/ShellUI.Components/Components/Toast.razor create mode 100644 src/ShellUI.Components/Components/Toggle.razor create mode 100644 src/ShellUI.Components/Components/Tooltip.razor diff --git a/src/ShellUI.Components/Components/Accordion.razor b/src/ShellUI.Components/Components/Accordion.razor new file mode 100644 index 0000000..3e00004 --- /dev/null +++ b/src/ShellUI.Components/Components/Accordion.razor @@ -0,0 +1,29 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private List _items = new(); + + public void RegisterItem(AccordionItem item) + { + _items.Add(item); + } + + public void UnregisterItem(AccordionItem item) + { + _items.Remove(item); + } +} + diff --git a/src/ShellUI.Components/Components/AccordionItem.razor b/src/ShellUI.Components/Components/AccordionItem.razor new file mode 100644 index 0000000..13d3cb6 --- /dev/null +++ b/src/ShellUI.Components/Components/AccordionItem.razor @@ -0,0 +1,54 @@ +@namespace ShellUI.Components + +
    + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@code { + [CascadingParameter] + private Accordion? Accordion { get; set; } + + [Parameter] + public string Title { get; set; } = ""; + + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + protected override void OnInitialized() + { + Accordion?.RegisterItem(this); + } + + public void Dispose() + { + Accordion?.UnregisterItem(this); + } + + private void Toggle() + { + IsOpen = !IsOpen; + } +} + diff --git a/src/ShellUI.Components/Components/Avatar.razor b/src/ShellUI.Components/Components/Avatar.razor new file mode 100644 index 0000000..6c274f8 --- /dev/null +++ b/src/ShellUI.Components/Components/Avatar.razor @@ -0,0 +1,51 @@ +@namespace ShellUI.Components + + + @if (!string.IsNullOrEmpty(Src)) + { + @Alt + } + else if (!string.IsNullOrEmpty(Fallback)) + { + + @Fallback + + } + else + { + + + + + + } + + +@code { + [Parameter] + public string? Src { get; set; } + + [Parameter] + public string? Alt { get; set; } + + [Parameter] + public string? Fallback { get; set; } + + [Parameter] + public string Size { get; set; } = "default"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string GetSizeClass() => Size switch + { + "sm" => "h-8 w-8", + "lg" => "h-12 w-12", + "xl" => "h-16 w-16", + _ => "h-10 w-10" + }; +} + diff --git a/src/ShellUI.Components/Components/Breadcrumb.razor b/src/ShellUI.Components/Components/Breadcrumb.razor new file mode 100644 index 0000000..103c284 --- /dev/null +++ b/src/ShellUI.Components/Components/Breadcrumb.razor @@ -0,0 +1,19 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/BreadcrumbItem.razor b/src/ShellUI.Components/Components/BreadcrumbItem.razor new file mode 100644 index 0000000..daea9cb --- /dev/null +++ b/src/ShellUI.Components/Components/BreadcrumbItem.razor @@ -0,0 +1,41 @@ +@namespace ShellUI.Components + +
  • + @if (!string.IsNullOrEmpty(Href)) + { + + @ChildContent + + } + else + { + + @ChildContent + + } + + @if (!IsLast) + { + + + + } +
  • + +@code { + [Parameter] + public string? Href { get; set; } + + [Parameter] + public bool IsLast { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/Checkbox.razor b/src/ShellUI.Components/Components/Checkbox.razor new file mode 100644 index 0000000..8ff0e9b --- /dev/null +++ b/src/ShellUI.Components/Components/Checkbox.razor @@ -0,0 +1,44 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public bool Checked { get; set; } + + [Parameter] + public EventCallback CheckedChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (!Disabled) + { + Checked = !Checked; + await CheckedChanged.InvokeAsync(Checked); + } + } +} + diff --git a/src/ShellUI.Components/Components/Collapsible.razor b/src/ShellUI.Components/Components/Collapsible.razor new file mode 100644 index 0000000..0494776 --- /dev/null +++ b/src/ShellUI.Components/Components/Collapsible.razor @@ -0,0 +1,46 @@ +@namespace ShellUI.Components + +
    + + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Toggle() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } +} + diff --git a/src/ShellUI.Components/Components/Combobox.razor b/src/ShellUI.Components/Components/Combobox.razor new file mode 100644 index 0000000..7eaeb83 --- /dev/null +++ b/src/ShellUI.Components/Components/Combobox.razor @@ -0,0 +1,71 @@ +@namespace ShellUI.Components + +
    + + + @if (IsOpen) + { +
    +
    + +
    +
    + @ChildContent +
    +
    + } +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] + public string Value { get; set; } = ""; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Select..."; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsOpen { get; set; } + private string searchQuery = ""; + + private void Toggle() => IsOpen = !IsOpen; + private void Close() => IsOpen = false; + + public async Task SelectValue(string value) + { + Value = value; + await ValueChanged.InvokeAsync(value); + IsOpen = false; + } +} + diff --git a/src/ShellUI.Components/Components/DatePicker.razor b/src/ShellUI.Components/Components/DatePicker.razor new file mode 100644 index 0000000..e8c5a65 --- /dev/null +++ b/src/ShellUI.Components/Components/DatePicker.razor @@ -0,0 +1,101 @@ +@namespace ShellUI.Components + +
    + + + @if (IsOpen) + { +
    +
    +
    + +
    @currentMonth.ToString("MMMM yyyy")
    + +
    +
    +
    Su
    +
    Mo
    +
    Tu
    +
    We
    +
    Th
    +
    Fr
    +
    Sa
    +
    +
    + @for (int i = 0; i < GetDaysInMonth(); i++) + { + var day = i + 1; + var date = new DateTime(currentMonth.Year, currentMonth.Month, day); + var isSelected = Value?.Date == date; + + } +
    +
    +
    + } +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] + public DateTime? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Pick a date"; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsOpen { get; set; } + private DateTime currentMonth = DateTime.Now; + + private void Toggle() => IsOpen = !IsOpen; + private void Close() => IsOpen = false; + + private async Task SelectDate(DateTime date) + { + Value = date; + await ValueChanged.InvokeAsync(date); + IsOpen = false; + } + + private void PreviousMonth() => currentMonth = currentMonth.AddMonths(-1); + private void NextMonth() => currentMonth = currentMonth.AddMonths(1); + private int GetDaysInMonth() => DateTime.DaysInMonth(currentMonth.Year, currentMonth.Month); +} + diff --git a/src/ShellUI.Components/Components/DateRangePicker.razor b/src/ShellUI.Components/Components/DateRangePicker.razor new file mode 100644 index 0000000..2aa9364 --- /dev/null +++ b/src/ShellUI.Components/Components/DateRangePicker.razor @@ -0,0 +1,195 @@ +@namespace ShellUI.Components + +
    + + } + + + +
    + + + @if (_isOpen) + { +
    +
    + +
    @_currentMonth.ToString("MMMM yyyy")
    + +
    +
    +
    Su
    +
    Mo
    +
    Tu
    +
    We
    +
    Th
    +
    Fr
    +
    Sa
    + @foreach (var day in GetCalendarDays()) + { + @if (day.HasValue) + { + + } + else + { +
    + } + } +
    +
    + } +
    + +@code { + [Parameter] + public DateTime? StartDate { get; set; } + + [Parameter] + public EventCallback StartDateChanged { get; set; } + + [Parameter] + public DateTime? EndDate { get; set; } + + [Parameter] + public EventCallback EndDateChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Pick a date range"; + + [Parameter] + public bool AllowClear { get; set; } = true; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isOpen; + private DateTime _currentMonth = DateTime.Now; + private bool _selectingStart = true; + + private string GetDisplayText() + { + if (StartDate.HasValue && EndDate.HasValue) + { + return $"{StartDate.Value:MMM dd} - {EndDate.Value:MMM dd, yyyy}"; + } + else if (StartDate.HasValue) + { + return $"{StartDate.Value:MMM dd, yyyy} - ..."; + } + return Placeholder; + } + + private string GetDateButtonClass(DateTime date) + { + var baseClass = "h-8 w-8 text-sm rounded-md hover:bg-accent transition-colors"; + + if (StartDate.HasValue && EndDate.HasValue) + { + if (date.Date == StartDate.Value.Date || date.Date == EndDate.Value.Date) + { + return baseClass + " bg-primary text-primary-foreground"; + } + if (date > StartDate.Value && date < EndDate.Value) + { + return baseClass + " bg-primary/20 text-primary-foreground"; + } + } + else if (StartDate.HasValue && date.Date == StartDate.Value.Date) + { + return baseClass + " bg-primary text-primary-foreground"; + } + + return baseClass; + } + + private void ToggleCalendar() => _isOpen = !_isOpen; + private void PreviousMonth() => _currentMonth = _currentMonth.AddMonths(-1); + private void NextMonth() => _currentMonth = _currentMonth.AddMonths(1); + + private async Task SelectDate(DateTime date) + { + if (_selectingStart || (!StartDate.HasValue && !EndDate.HasValue)) + { + StartDate = date; + EndDate = null; + _selectingStart = false; + await StartDateChanged.InvokeAsync(date); + } + else + { + if (date < StartDate) + { + EndDate = StartDate; + StartDate = date; + await StartDateChanged.InvokeAsync(date); + await EndDateChanged.InvokeAsync(EndDate); + } + else + { + EndDate = date; + await EndDateChanged.InvokeAsync(date); + } + _isOpen = false; + _selectingStart = true; + } + } + + private async Task ClearRange() + { + StartDate = null; + EndDate = null; + _selectingStart = true; + _isOpen = false; + await StartDateChanged.InvokeAsync(null); + await EndDateChanged.InvokeAsync(null); + } + + private List GetCalendarDays() + { + var days = new List(); + var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); + var lastDay = firstDay.AddMonths(1).AddDays(-1); + var startDayOfWeek = (int)firstDay.DayOfWeek; + + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); + for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); + + return days; + } +} diff --git a/src/ShellUI.Components/Components/Drawer.razor b/src/ShellUI.Components/Components/Drawer.razor new file mode 100644 index 0000000..3519739 --- /dev/null +++ b/src/ShellUI.Components/Components/Drawer.razor @@ -0,0 +1,74 @@ +@namespace ShellUI.Components + +@if (IsOpen) +{ +
    + +
    +
    + +
    + @if (!string.IsNullOrEmpty(Title)) + { +
    +

    @Title

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

    @Description

    + } +
    +
    + @ChildContent +
    +
    +} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Description { get; set; } + + [Parameter] + public string Side { get; set; } = "bottom"; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private string GetPositionClass() => Side switch + { + "left" => "inset-y-0 left-0 h-full w-3/4 sm:max-w-sm slide-in-from-left", + "right" => "inset-y-0 right-0 h-full w-3/4 sm:max-w-sm slide-in-from-right", + "top" => "inset-x-0 top-0 slide-in-from-top", + _ => "inset-x-0 bottom-0 slide-in-from-bottom" + }; +} + diff --git a/src/ShellUI.Components/Components/Dropdown.razor b/src/ShellUI.Components/Components/Dropdown.razor new file mode 100644 index 0000000..a26a094 --- /dev/null +++ b/src/ShellUI.Components/Components/Dropdown.razor @@ -0,0 +1,44 @@ +@namespace ShellUI.Components + +
    + + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@code { + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task ToggleOpen() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } +} + diff --git a/src/ShellUI.Components/Components/Form.razor b/src/ShellUI.Components/Components/Form.razor new file mode 100644 index 0000000..72022cc --- /dev/null +++ b/src/ShellUI.Components/Components/Form.razor @@ -0,0 +1,30 @@ +@namespace ShellUI.Components +@using Microsoft.AspNetCore.Components.Forms + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnValidSubmit { get; set; } + + [Parameter] + public EventCallback OnInvalidSubmit { get; set; } + + [Parameter] + public string ClassName { get; set; } = "space-y-6"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleSubmit() + { + // Basic form submission - users can add EditContext for validation + await Task.CompletedTask; + } +} + diff --git a/src/ShellUI.Components/Components/InputOTP.razor b/src/ShellUI.Components/Components/InputOTP.razor new file mode 100644 index 0000000..6a541d1 --- /dev/null +++ b/src/ShellUI.Components/Components/InputOTP.razor @@ -0,0 +1,146 @@ +@namespace ShellUI.Components +@using Microsoft.JSInterop +@inject IJSRuntime JS + +
    + @for (int i = 0; i < Length; i++) + { + var index = i; + + @if (GroupBy > 0 && i > 0 && i % GroupBy == 0) + { + - + } + + + } +
    + +@code { + [Parameter] + public string Value { get; set; } = ""; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public EventCallback OnComplete { get; set; } + + [Parameter] + public int Length { get; set; } = 6; + + [Parameter] + public int GroupBy { get; set; } = 3; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private int _focusedIndex = -1; + private ElementReference[] _inputRefs = null!; + private string _id = Guid.NewGuid().ToString("N")[..8]; + + protected override void OnInitialized() + { + _inputRefs = new ElementReference[Length]; + } + + private string GetDigit(int index) => index < Value.Length ? Value[index].ToString() : ""; + + private async Task HandleInput(int index, ChangeEventArgs e) + { + var input = e.Value?.ToString() ?? ""; + if (string.IsNullOrEmpty(input) || !char.IsDigit(input[0])) + { + return; + } + + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index] = input[0]; + Value = new string(chars).TrimEnd(); + + await ValueChanged.InvokeAsync(Value); + + if (index < Length - 1) + { + await FocusInput(index + 1); + } + + if (Value.Replace(" ", "").Length == Length) + { + await OnComplete.InvokeAsync(Value); + } + } + + private async Task HandleKeyDown(int index, KeyboardEventArgs e) + { + if (e.Key == "Backspace") + { + if (string.IsNullOrEmpty(GetDigit(index)) && index > 0) + { + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index - 1] = ' '; + Value = new string(chars).TrimEnd(); + await ValueChanged.InvokeAsync(Value); + await FocusInput(index - 1); + } + else if (!string.IsNullOrEmpty(GetDigit(index))) + { + var newValue = Value.PadRight(Length, ' '); + var chars = newValue.ToCharArray(); + chars[index] = ' '; + Value = new string(chars).TrimEnd(); + await ValueChanged.InvokeAsync(Value); + } + } + else if (e.Key == "ArrowLeft" && index > 0) + { + await FocusInput(index - 1); + } + else if (e.Key == "ArrowRight" && index < Length - 1) + { + await FocusInput(index + 1); + } + } + + private async Task HandlePaste(ClipboardEventArgs e) + { + // Paste handling will be done via JS in a real implementation + await Task.CompletedTask; + } + + private async Task FocusInput(int index) + { + if (index >= 0 && index < Length) + { + try + { + await JS.InvokeVoidAsync("eval", $"document.getElementById('otp-input-{_id}-{index}')?.focus()"); + } + catch + { + // Ignore JS interop errors + } + } + } +} + diff --git a/src/ShellUI.Components/Components/Menubar.razor b/src/ShellUI.Components/Components/Menubar.razor new file mode 100644 index 0000000..330edd2 --- /dev/null +++ b/src/ShellUI.Components/Components/Menubar.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/MenubarItem.razor b/src/ShellUI.Components/Components/MenubarItem.razor new file mode 100644 index 0000000..eef316b --- /dev/null +++ b/src/ShellUI.Components/Components/MenubarItem.razor @@ -0,0 +1,41 @@ +@namespace ShellUI.Components + +
    + + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] + public string Title { get; set; } = ""; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsOpen { get; set; } + + private void Toggle() => IsOpen = !IsOpen; + private void Close() => IsOpen = false; +} + diff --git a/src/ShellUI.Components/Components/Navbar.razor b/src/ShellUI.Components/Components/Navbar.razor new file mode 100644 index 0000000..6d39e17 --- /dev/null +++ b/src/ShellUI.Components/Components/Navbar.razor @@ -0,0 +1,19 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/NavigationMenu.razor b/src/ShellUI.Components/Components/NavigationMenu.razor new file mode 100644 index 0000000..9e8f02a --- /dev/null +++ b/src/ShellUI.Components/Components/NavigationMenu.razor @@ -0,0 +1,19 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/NavigationMenuItem.razor b/src/ShellUI.Components/Components/NavigationMenuItem.razor new file mode 100644 index 0000000..6d76691 --- /dev/null +++ b/src/ShellUI.Components/Components/NavigationMenuItem.razor @@ -0,0 +1,44 @@ +@namespace ShellUI.Components + +
    + + + @if (IsOpen && HasContent) + { +
    + @ChildContent +
    + } +
    + +@code { + [Parameter] + public string Title { get; set; } = ""; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public bool HasContent { get; set; } = true; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsOpen { get; set; } + + private void Toggle() => IsOpen = !IsOpen; +} + diff --git a/src/ShellUI.Components/Components/Pagination.razor b/src/ShellUI.Components/Components/Pagination.razor new file mode 100644 index 0000000..a918943 --- /dev/null +++ b/src/ShellUI.Components/Components/Pagination.razor @@ -0,0 +1,83 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public int CurrentPage { get; set; } = 1; + + [Parameter] + public EventCallback CurrentPageChanged { get; set; } + + [Parameter] + public int TotalPages { get; set; } = 1; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task GoToPage(int page) + { + CurrentPage = page; + await CurrentPageChanged.InvokeAsync(CurrentPage); + } + + private async Task PreviousPage() + { + if (CurrentPage > 1) + { + CurrentPage--; + await CurrentPageChanged.InvokeAsync(CurrentPage); + } + } + + private async Task NextPage() + { + if (CurrentPage < TotalPages) + { + CurrentPage++; + await CurrentPageChanged.InvokeAsync(CurrentPage); + } + } +} + diff --git a/src/ShellUI.Components/Components/Popover.razor b/src/ShellUI.Components/Components/Popover.razor new file mode 100644 index 0000000..ed1d2fa --- /dev/null +++ b/src/ShellUI.Components/Components/Popover.razor @@ -0,0 +1,63 @@ +@namespace ShellUI.Components + +
    +
    + @Trigger +
    + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Placement { get; set; } = "bottom"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Toggle() + { + IsOpen = !IsOpen; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private async Task Close() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private string GetPlacementClass() => Placement switch + { + "top" => "bottom-full left-0 mb-2", + "left" => "right-full top-0 mr-2", + "right" => "left-full top-0 ml-2", + _ => "top-full left-0 mt-2" + }; +} + diff --git a/src/ShellUI.Components/Components/Progress.razor b/src/ShellUI.Components/Components/Progress.razor new file mode 100644 index 0000000..f69086f --- /dev/null +++ b/src/ShellUI.Components/Components/Progress.razor @@ -0,0 +1,20 @@ +@namespace ShellUI.Components + +
    +
    +
    + +@code { + [Parameter] + public int Value { get; set; } = 0; + + [Parameter] + public string Height { get; set; } = "0.5rem"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/RadioGroup.razor b/src/ShellUI.Components/Components/RadioGroup.razor new file mode 100644 index 0000000..3480436 --- /dev/null +++ b/src/ShellUI.Components/Components/RadioGroup.razor @@ -0,0 +1,32 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
    + +@code { + [Parameter] + public string? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + public async Task SetValue(string value) + { + if (Value != value) + { + Value = value; + await ValueChanged.InvokeAsync(value); + } + } +} + diff --git a/src/ShellUI.Components/Components/RadioGroupItem.razor b/src/ShellUI.Components/Components/RadioGroupItem.razor new file mode 100644 index 0000000..2170853 --- /dev/null +++ b/src/ShellUI.Components/Components/RadioGroupItem.razor @@ -0,0 +1,52 @@ +@namespace ShellUI.Components + +
    + + @if (ChildContent != null) + { + + } +
    + +@code { + [CascadingParameter] + private RadioGroup? RadioGroup { get; set; } + + [Parameter] + public string Value { get; set; } = ""; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsChecked => RadioGroup?.Value == Value; + + private async Task OnClick() + { + if (RadioGroup != null) + { + await RadioGroup.SetValue(Value); + } + } +} + diff --git a/src/ShellUI.Components/Components/Resizable.razor b/src/ShellUI.Components/Components/Resizable.razor new file mode 100644 index 0000000..0e6b1ad --- /dev/null +++ b/src/ShellUI.Components/Components/Resizable.razor @@ -0,0 +1,20 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Direction { get; set; } = "horizontal"; + + [Parameter] + public string ClassName { get; set; } = "h-full w-full"; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/ScrollArea.razor b/src/ShellUI.Components/Components/ScrollArea.razor new file mode 100644 index 0000000..2d4a9fe --- /dev/null +++ b/src/ShellUI.Components/Components/ScrollArea.razor @@ -0,0 +1,22 @@ +@namespace ShellUI.Components + +
    +
    + @ChildContent +
    +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Height { get; set; } = "400px"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/Select.razor b/src/ShellUI.Components/Components/Select.razor new file mode 100644 index 0000000..b8fa84e --- /dev/null +++ b/src/ShellUI.Components/Components/Select.razor @@ -0,0 +1,37 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public string? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleChange(ChangeEventArgs e) + { + Value = e.Value?.ToString(); + await ValueChanged.InvokeAsync(Value); + } +} + diff --git a/src/ShellUI.Components/Components/Separator.razor b/src/ShellUI.Components/Components/Separator.razor new file mode 100644 index 0000000..65e617f --- /dev/null +++ b/src/ShellUI.Components/Components/Separator.razor @@ -0,0 +1,15 @@ +@namespace ShellUI.Components + +
    + +@code { + [Parameter] + public string Orientation { get; set; } = "horizontal"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/Sheet.razor b/src/ShellUI.Components/Components/Sheet.razor new file mode 100644 index 0000000..9a6fabf --- /dev/null +++ b/src/ShellUI.Components/Components/Sheet.razor @@ -0,0 +1,72 @@ +@namespace ShellUI.Components + +@if (IsOpen) +{ +
    + +
    +
    + @if (!string.IsNullOrEmpty(Title)) + { +
    +

    @Title

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

    @Description

    + } +
    +
    + @ChildContent +
    +
    +} + +@code { + [Parameter] + public bool IsOpen { get; set; } + + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Description { get; set; } + + [Parameter] + public string Side { get; set; } = "right"; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + IsOpen = false; + await IsOpenChanged.InvokeAsync(IsOpen); + } + + private string GetPositionClass() => Side switch + { + "left" => "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm slide-in-from-left", + "top" => "inset-x-0 top-0 border-b slide-in-from-top", + "bottom" => "inset-x-0 bottom-0 border-t slide-in-from-bottom", + _ => "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm slide-in-from-right" + }; +} + diff --git a/src/ShellUI.Components/Components/Sidebar.razor b/src/ShellUI.Components/Components/Sidebar.razor new file mode 100644 index 0000000..10163b5 --- /dev/null +++ b/src/ShellUI.Components/Components/Sidebar.razor @@ -0,0 +1,22 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public bool IsOpen { get; set; } = true; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/Skeleton.razor b/src/ShellUI.Components/Components/Skeleton.razor new file mode 100644 index 0000000..e094135 --- /dev/null +++ b/src/ShellUI.Components/Components/Skeleton.razor @@ -0,0 +1,15 @@ +@namespace ShellUI.Components + +
    + +@code { + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/Slider.razor b/src/ShellUI.Components/Components/Slider.razor new file mode 100644 index 0000000..ee1456c --- /dev/null +++ b/src/ShellUI.Components/Components/Slider.razor @@ -0,0 +1,48 @@ +@namespace ShellUI.Components + +
    + +
    + +@code { + [Parameter] + public double Value { get; set; } = 50; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public double Min { get; set; } = 0; + + [Parameter] + public double Max { get; set; } = 100; + + [Parameter] + public double Step { get; set; } = 1; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleInput(ChangeEventArgs args) + { + if (double.TryParse(args.Value?.ToString(), out var newValue)) + { + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + } +} + diff --git a/src/ShellUI.Components/Components/Switch.razor b/src/ShellUI.Components/Components/Switch.razor new file mode 100644 index 0000000..48eb57b --- /dev/null +++ b/src/ShellUI.Components/Components/Switch.razor @@ -0,0 +1,39 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public bool Checked { get; set; } + + [Parameter] + public EventCallback CheckedChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (!Disabled) + { + Checked = !Checked; + await CheckedChanged.InvokeAsync(Checked); + } + } +} + diff --git a/src/ShellUI.Components/Components/Table.razor b/src/ShellUI.Components/Components/Table.razor new file mode 100644 index 0000000..184adaa --- /dev/null +++ b/src/ShellUI.Components/Components/Table.razor @@ -0,0 +1,19 @@ +@namespace ShellUI.Components + +
    + + @ChildContent +
    +
    + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/TableBody.razor b/src/ShellUI.Components/Components/TableBody.razor new file mode 100644 index 0000000..b3da14f --- /dev/null +++ b/src/ShellUI.Components/Components/TableBody.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/TableCell.razor b/src/ShellUI.Components/Components/TableCell.razor new file mode 100644 index 0000000..135f9be --- /dev/null +++ b/src/ShellUI.Components/Components/TableCell.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/TableHead.razor b/src/ShellUI.Components/Components/TableHead.razor new file mode 100644 index 0000000..97125cf --- /dev/null +++ b/src/ShellUI.Components/Components/TableHead.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/TableHeader.razor b/src/ShellUI.Components/Components/TableHeader.razor new file mode 100644 index 0000000..400bee9 --- /dev/null +++ b/src/ShellUI.Components/Components/TableHeader.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/TableRow.razor b/src/ShellUI.Components/Components/TableRow.razor new file mode 100644 index 0000000..3e32261 --- /dev/null +++ b/src/ShellUI.Components/Components/TableRow.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/Tabs.razor b/src/ShellUI.Components/Components/Tabs.razor new file mode 100644 index 0000000..256d706 --- /dev/null +++ b/src/ShellUI.Components/Components/Tabs.razor @@ -0,0 +1,25 @@ +@namespace ShellUI.Components + +
    +
    + @TabHeaders +
    +
    + @TabContent +
    +
    + +@code { + [Parameter] + public RenderFragment? TabHeaders { get; set; } + + [Parameter] + public RenderFragment? TabContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} + diff --git a/src/ShellUI.Components/Components/TimePicker.razor b/src/ShellUI.Components/Components/TimePicker.razor new file mode 100644 index 0000000..0c42983 --- /dev/null +++ b/src/ShellUI.Components/Components/TimePicker.razor @@ -0,0 +1,100 @@ +@namespace ShellUI.Components + +
    + + + @if (IsOpen) + { +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    + } +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] + public TimeSpan? Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public string Placeholder { get; set; } = "Pick a time"; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool IsOpen { get; set; } + private int selectedHour = 0; + private int selectedMinute = 0; + + protected override void OnInitialized() + { + if (Value.HasValue) + { + selectedHour = Value.Value.Hours; + selectedMinute = Value.Value.Minutes; + } + } + + private void Toggle() => IsOpen = !IsOpen; + private void Close() => IsOpen = false; + + private async Task ApplyTime() + { + Value = new TimeSpan(selectedHour, selectedMinute, 0); + await ValueChanged.InvokeAsync(Value); + IsOpen = false; + } +} + diff --git a/src/ShellUI.Components/Components/Toast.razor b/src/ShellUI.Components/Components/Toast.razor new file mode 100644 index 0000000..22ee0e9 --- /dev/null +++ b/src/ShellUI.Components/Components/Toast.razor @@ -0,0 +1,78 @@ +@namespace ShellUI.Components + +@if (IsVisible) +{ +
    +
    +
    + @if (!string.IsNullOrEmpty(Title)) + { +
    @Title
    + } + @if (!string.IsNullOrEmpty(Description)) + { +
    @Description
    + } + @ChildContent +
    + +
    +
    +} + +@code { + [Parameter] + public bool IsVisible { get; set; } + + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Description { get; set; } + + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string Position { get; set; } = "bottom-right"; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + IsVisible = false; + await IsVisibleChanged.InvokeAsync(IsVisible); + } + + private string GetVariantClass() => Variant switch + { + "destructive" => "border-destructive bg-destructive text-destructive-foreground", + "success" => "border-green-500 bg-green-50 text-green-900 dark:bg-green-900/20 dark:text-green-100", + _ => "bg-background text-foreground" + }; + + private string GetPositionClass() => Position switch + { + "top-left" => "top-0 left-0", + "top-right" => "top-0 right-0", + "bottom-left" => "bottom-0 left-0", + _ => "bottom-0 right-0" + }; +} + diff --git a/src/ShellUI.Components/Components/Toggle.razor b/src/ShellUI.Components/Components/Toggle.razor new file mode 100644 index 0000000..acfbddc --- /dev/null +++ b/src/ShellUI.Components/Components/Toggle.razor @@ -0,0 +1,55 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] + public bool Pressed { get; set; } + + [Parameter] + public EventCallback PressedChanged { get; set; } + + [Parameter] + public string Variant { get; set; } = "default"; + + [Parameter] + public string Size { get; set; } = "default"; + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + Pressed = !Pressed; + await PressedChanged.InvokeAsync(Pressed); + } + + private string GetVariantClass() => Variant switch + { + "outline" => Pressed ? "bg-accent" : "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + _ => Pressed ? "bg-accent text-accent-foreground" : "bg-transparent hover:bg-muted hover:text-muted-foreground" + }; + + private string GetSizeClass() => Size switch + { + "sm" => "h-9 px-2.5", + "lg" => "h-11 px-5", + _ => "h-10 px-3" + }; +} + diff --git a/src/ShellUI.Components/Components/Tooltip.razor b/src/ShellUI.Components/Components/Tooltip.razor new file mode 100644 index 0000000..a0fe65c --- /dev/null +++ b/src/ShellUI.Components/Components/Tooltip.razor @@ -0,0 +1,46 @@ +@namespace ShellUI.Components + +
    + @Trigger + + @if (_isVisible) + { +
    + @Content +
    + } +
    + +@code { + [Parameter] + public RenderFragment? Trigger { get; set; } + + [Parameter] + public RenderFragment? Content { get; set; } + + [Parameter] + public string Placement { get; set; } = "top"; + + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isVisible; + + private void Show() => _isVisible = true; + private void Hide() => _isVisible = false; + + private string GetPlacementClass() => Placement switch + { + "bottom" => "top-full left-1/2 -translate-x-1/2 mt-2", + "left" => "right-full top-1/2 -translate-y-1/2 mr-2", + "right" => "left-full top-1/2 -translate-y-1/2 ml-2", + _ => "bottom-full left-1/2 -translate-x-1/2 mb-2" + }; +} + From 75c668ae89db11234a4dd9195d5caa6c998363d6 Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 01:30:54 +0200 Subject: [PATCH 36/45] feat: Expand ComponentRegistry with additional UI components including Skeleton, Progress, Select, Checkbox, Switch, Tabs, Navbar, Sidebar, Dropdown, RadioGroup, Slider, Accordion, Breadcrumb, Toast, Tooltip, and more, enhancing the ShellUI component library and providing greater flexibility for developers. --- COMPONENT_ROADMAP.md | 194 +++++++++++++++++++++ src/ShellUI.Templates/ComponentRegistry.cs | 90 +++++++++- 2 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 COMPONENT_ROADMAP.md diff --git a/COMPONENT_ROADMAP.md b/COMPONENT_ROADMAP.md new file mode 100644 index 0000000..a5c8a1b --- /dev/null +++ b/COMPONENT_ROADMAP.md @@ -0,0 +1,194 @@ +# ShellUI Component Roadmap + +**Goal:** Build ALL components from shadcn/ui + sysinfocus to create the most comprehensive Blazor UI library + +## Current Status: 53/70+ Components ✅ (76% Complete!) + +### ✅ Completed (53) +1. Accordion +2. AccordionItem +3. Alert +4. Avatar +5. Badge +6. Breadcrumb +7. BreadcrumbItem +8. Button +9. Card +10. Checkbox +11. Collapsible **NEW** +12. Combobox **NEW** +13. DatePicker **NEW** +14. Dialog +15. Drawer **NEW** +16. Dropdown +17. Form **NEW** +18. Input +19. InputOTP **NEW** +20. Label +21. Menubar **NEW** +22. MenubarItem **NEW** +23. Navbar +24. NavigationMenu **NEW** +25. NavigationMenuItem **NEW** +26. Pagination **NEW** +27. Popover +28. Progress +29. RadioGroup +30. RadioGroupItem +31. Resizable **NEW** +32. ScrollArea **NEW** +33. Select +34. Separator +35. Sheet **NEW** +36. Sidebar +37. Skeleton +38. Slider +39. Switch +40. Table +41. TableBody +42. TableCell +43. TableHead +44. TableHeader +45. TableRow +46. Tabs +47. Textarea +48. Theme Toggle +49. TimePicker **NEW** +50. Toast +51. Toggle +52. Tooltip +53. DateRangePicker **NEW** + +--- + +## Phase 1: Core Form Components ✅ COMPLETED +**Target: Q4 2025 - Q1 2026** + +- [x] **Form** - Form wrapper with validation +- [x] **Input OTP** - One-time password input +- [x] **Combobox** - Autocomplete select input +- [x] **Date Picker** - Calendar date selection +- [x] **Time Picker** - Time selection input + +--- + +## Phase 2: Layout & Navigation ✅ COMPLETED +**Target: Q1 2026** + +- [x] **Navigation Menu** - Main navigation menu +- [x] **Menubar** - Application menubar +- [x] **Pagination** - Page navigation controls +- [x] **Scroll Area** - Custom scrollable container +- [x] **Resizable** - Resizable panels +- [x] **Sheet** - Side panel/drawer +- [x] **Drawer** - Sliding drawer panel +- [x] **Collapsible** - Collapsible content + +--- + +## Phase 3: Data Display (Priority: MEDIUM) +**Target: Q1 2026** + +- [ ] **Data Table** - Advanced data table with sorting/filtering +- [ ] **Calendar** - Full calendar component +- [ ] **Chart** - Chart/graph components +- [ ] **Tree View** - Hierarchical tree structure +- [ ] **Timeline** - Event timeline +- [ ] **Stepper** - Step-by-step wizard + +--- + +## Phase 4: Feedback & Overlay (Priority: MEDIUM) +**Target: Q2 2026** + +- [ ] **Alert Dialog** - Confirmation dialogs +- [ ] **Hover Card** - Rich hover content +- [ ] **Context Menu** - Right-click context menu +- [ ] **Command** - Command palette (Cmd+K) +- [ ] **Loading** - Loading spinner +- [ ] **Empty State** - Empty state placeholder + +--- + +## Phase 5: Advanced Components (Priority: LOW) +**Target: Q2-Q3 2026** + +- [ ] **Carousel** - Image/content carousel +- [ ] **Aspect Ratio** - Aspect ratio container +- [ ] **Code Block** - Syntax highlighted code +- [ ] **Markdown** - Markdown renderer +- [ ] **File Upload** - File upload component +- [ ] **Color Picker** - Color selection +- [ ] **Rich Text Editor** - WYSIWYG editor +- [ ] **Kanban Board** - Drag-and-drop board + +--- + +## Phase 6: Blazor-Specific Components +**Target: Q3 2026** + +- [ ] **Virtual Scroll** - Virtualized list +- [ ] **Grid** - Responsive grid layout +- [ ] **Split View** - Split pane view +- [ ] **PDF Viewer** - PDF display +- [ ] **Video Player** - Video playback +- [ ] **Audio Player** - Audio playback +- [ ] **QR Code** - QR code generator +- [ ] **Barcode** - Barcode scanner + +--- + +## Summary by Category + +### Form (11 components) ✅ +Button ✅, Input ✅, Textarea ✅, Select ✅, Checkbox ✅, RadioGroup ✅, RadioGroupItem ✅, Switch ✅, Toggle ✅, Label ✅, Slider ✅ + +### Layout (12 components) ✅ +Card ✅, Tabs ✅, Navbar ✅, Sidebar ✅, Separator ✅, Accordion ✅, AccordionItem ✅, Breadcrumb ✅, BreadcrumbItem ✅ + +### Feedback (5 components) ✅ +Alert ✅, Progress ✅, Skeleton ✅, Toast ✅, Tooltip ✅ + +### Overlay (3 components) ✅ +Dialog ✅, Dropdown ✅, Popover ✅ + +### Data Display (8 components) ✅ +Badge ✅, Avatar ✅, Table ✅, TableHeader ✅, TableBody ✅, TableRow ✅, TableHead ✅, TableCell ✅ + +### Utility (1 component) ✅ +Theme Toggle ✅ + +--- + +## Milestones + +- **M1 (COMPLETED):** 37 components ✅ 🎉 +- **M2 (Q1 2026):** 50 components (+ Phase 1 & 2 essentials) +- **M3 (Q2 2026):** 60 components (+ Phase 3 & 4) +- **M4 (Q3 2026):** 70+ components (+ Phase 5 & 6) + +**Current Progress: 37/70+ components (53% complete!)** 🎯 + +--- + +## Recent Additions (Latest Session) + +### Session 1 (Q4 2025) +**Added 18 New Components:** +- RadioGroup + RadioGroupItem (Form) +- Slider (Form) +- Toggle (Form) +- Accordion + AccordionItem (Layout) +- Breadcrumb + BreadcrumbItem (Layout) +- Toast (Feedback) +- Tooltip (Feedback) +- Popover (Overlay) +- Avatar (Data Display) +- Table + TableHeader + TableBody + TableRow + TableHead + TableCell (Data Display) + +All components: +- ✅ Built with inline ternary styling +- ✅ Full Tailwind CSS integration +- ✅ Dark mode support +- ✅ CLI installable +- ✅ Demo page ready diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index 8b8cc79..0aaa206 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -15,7 +15,51 @@ public static class ComponentRegistry { "badge", BadgeTemplate.Metadata }, { "label", LabelTemplate.Metadata }, { "textarea", TextareaTemplate.Metadata }, - { "dialog", DialogTemplate.Metadata } + { "dialog", DialogTemplate.Metadata }, + { "skeleton", SkeletonTemplate.Metadata }, + { "progress", ProgressTemplate.Metadata }, + { "separator", SeparatorTemplate.Metadata }, + { "select", SelectTemplate.Metadata }, + { "checkbox", CheckboxTemplate.Metadata }, + { "switch", SwitchTemplate.Metadata }, + { "tabs", TabsTemplate.Metadata }, + { "navbar", NavbarTemplate.Metadata }, + { "sidebar", SidebarTemplate.Metadata }, + { "dropdown", DropdownTemplate.Metadata }, + { "radio-group", RadioGroupTemplate.Metadata }, + { "radio-group-item", RadioGroupItemTemplate.Metadata }, + { "slider", SliderTemplate.Metadata }, + { "toggle", ToggleTemplate.Metadata }, + { "accordion", AccordionTemplate.Metadata }, + { "accordion-item", AccordionItemTemplate.Metadata }, + { "breadcrumb", BreadcrumbTemplate.Metadata }, + { "breadcrumb-item", BreadcrumbItemTemplate.Metadata }, + { "toast", ToastTemplate.Metadata }, + { "tooltip", TooltipTemplate.Metadata }, + { "popover", PopoverTemplate.Metadata }, + { "avatar", AvatarTemplate.Metadata }, + { "table", TableTemplate.Metadata }, + { "table-header", TableHeaderTemplate.Metadata }, + { "table-body", TableBodyTemplate.Metadata }, + { "table-row", TableRowTemplate.Metadata }, + { "table-head", TableHeadTemplate.Metadata }, + { "table-cell", TableCellTemplate.Metadata }, + { "form", FormTemplate.Metadata }, + { "input-otp", InputOTPTemplate.Metadata }, + { "combobox", ComboboxTemplate.Metadata }, + { "date-picker", DatePickerTemplate.Metadata }, + { "date-range-picker", DateRangePickerTemplate.Metadata }, + { "time-picker", TimePickerTemplate.Metadata }, + { "navigation-menu", NavigationMenuTemplate.Metadata }, + { "navigation-menu-item", NavigationMenuItemTemplate.Metadata }, + { "menubar", MenubarTemplate.Metadata }, + { "menubar-item", MenubarItemTemplate.Metadata }, + { "pagination", PaginationTemplate.Metadata }, + { "scroll-area", ScrollAreaTemplate.Metadata }, + { "resizable", ResizableTemplate.Metadata }, + { "sheet", SheetTemplate.Metadata }, + { "drawer", DrawerTemplate.Metadata }, + { "collapsible", CollapsibleTemplate.Metadata } }; public static string? GetComponentContent(string componentName) @@ -31,6 +75,50 @@ public static class ComponentRegistry "label" => LabelTemplate.Content, "textarea" => TextareaTemplate.Content, "dialog" => DialogTemplate.Content, + "skeleton" => SkeletonTemplate.Content, + "progress" => ProgressTemplate.Content, + "separator" => SeparatorTemplate.Content, + "select" => SelectTemplate.Content, + "checkbox" => CheckboxTemplate.Content, + "switch" => SwitchTemplate.Content, + "tabs" => TabsTemplate.Content, + "navbar" => NavbarTemplate.Content, + "sidebar" => SidebarTemplate.Content, + "dropdown" => DropdownTemplate.Content, + "radio-group" => RadioGroupTemplate.Content, + "radio-group-item" => RadioGroupItemTemplate.Content, + "slider" => SliderTemplate.Content, + "toggle" => ToggleTemplate.Content, + "accordion" => AccordionTemplate.Content, + "accordion-item" => AccordionItemTemplate.Content, + "breadcrumb" => BreadcrumbTemplate.Content, + "breadcrumb-item" => BreadcrumbItemTemplate.Content, + "toast" => ToastTemplate.Content, + "tooltip" => TooltipTemplate.Content, + "popover" => PopoverTemplate.Content, + "avatar" => AvatarTemplate.Content, + "table" => TableTemplate.Content, + "table-header" => TableHeaderTemplate.Content, + "table-body" => TableBodyTemplate.Content, + "table-row" => TableRowTemplate.Content, + "table-head" => TableHeadTemplate.Content, + "table-cell" => TableCellTemplate.Content, + "form" => FormTemplate.Content, + "input-otp" => InputOTPTemplate.Content, + "combobox" => ComboboxTemplate.Content, + "date-picker" => DatePickerTemplate.Content, + "date-range-picker" => DateRangePickerTemplate.Content, + "time-picker" => TimePickerTemplate.Content, + "navigation-menu" => NavigationMenuTemplate.Content, + "navigation-menu-item" => NavigationMenuItemTemplate.Content, + "menubar" => MenubarTemplate.Content, + "menubar-item" => MenubarItemTemplate.Content, + "pagination" => PaginationTemplate.Content, + "scroll-area" => ScrollAreaTemplate.Content, + "resizable" => ResizableTemplate.Content, + "sheet" => SheetTemplate.Content, + "drawer" => DrawerTemplate.Content, + "collapsible" => CollapsibleTemplate.Content, _ => null }; } From eeef10111f93ea9a1f97154b2a67ff0b23b02b7f Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 01:57:04 +0200 Subject: [PATCH 37/45] fix: Update TimePicker component to display time in 24-hour format and adjust minute selection logic for improved functionality --- NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor | 4 ++-- src/ShellUI.Templates/Templates/TimePickerTemplate.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor b/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor index 6ef3fdd..170bae8 100644 --- a/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor +++ b/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor @@ -6,7 +6,7 @@ disabled="@Disabled" class="@("flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 " + (Disabled ? "opacity-50 cursor-not-allowed" : ""))"> - @(SelectedTime?.ToString("hh:mm tt") ?? Placeholder) + @(SelectedTime?.ToString(@"hh\:mm") ?? Placeholder) @@ -29,7 +29,7 @@
    - @for (int m = 0; m < 60; m += Step) + @for (int m = 0; m < 60; m++) { } From 33443a2e9fb8f3f742baf6eb8782c997bfe8a843 Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 23:09:57 +0200 Subject: [PATCH 38/45] feat: Revamp MainLayout with a new Navbar component, enhancing navigation and mobile responsiveness, and introduce a Search component for improved user experience in finding components. --- .../Components/Layout/MainLayout.razor | 143 ++++++++++- .../Components/UI/Search.razor | 228 ++++++++++++++++++ 2 files changed, 359 insertions(+), 12 deletions(-) create mode 100644 NET9/BlazorInteractiveServer/Components/UI/Search.razor diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index 6b331f8..8ef7b18 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -1,21 +1,126 @@ @inherits LayoutComponentBase
    -
    -
    -
    -

    ShellUI

    -
    + }
    @Body @@ -32,4 +137,18 @@ An unhandled error has occurred. Reload x -
    \ No newline at end of file +
    + +@code { + private bool _isMobileMenuOpen = false; + + private void ToggleMobileMenu() + { + _isMobileMenuOpen = !_isMobileMenuOpen; + } + + private void CloseMobileMenu() + { + _isMobileMenuOpen = false; + } +} \ No newline at end of file diff --git a/NET9/BlazorInteractiveServer/Components/UI/Search.razor b/NET9/BlazorInteractiveServer/Components/UI/Search.razor new file mode 100644 index 0000000..30b5b70 --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/Search.razor @@ -0,0 +1,228 @@ +@namespace BlazorInteractiveServer.Components.UI +@using Microsoft.JSInterop +@inject IJSRuntime JS + + + + + +@if (_isOpen) +{ +
    + +
    + + +
    +
    + +
    + + + + + ESC +
    + + +
    + @if (string.IsNullOrEmpty(SearchQuery)) + { + +
    + + + +

    Type to search for components

    +
    + } + else + { + + @foreach (var result in GetSearchResults()) + { + + } + + @if (!GetSearchResults().Any()) + { +
    +

    No components found for "@SearchQuery"

    +
    + } + } +
    + + +
    +
    +
    + + ↑↓ + to navigate + + + + to select + +
    + + ESC + to close + +
    +
    +
    +
    +
    +} + +@code { + [Parameter] + public string ClassName { get; set; } = ""; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + [Parameter] + public EventCallback OnComponentSelected { get; set; } + + private bool _isOpen = false; + private string SearchQuery = ""; + private ElementReference _searchInput; + + // Sample component data - in real implementation, this would come from a service + private readonly List _components = new() + { + new("Button", "A clickable button component with multiple variants", "Form"), + new("Input", "A text input field with validation support", "Form"), + new("Card", "A flexible container for grouping related content", "Layout"), + new("Dialog", "A modal dialog for important interactions", "Overlay"), + new("Alert", "A banner for displaying important messages", "Feedback"), + new("Badge", "A small status indicator or label", "Data Display"), + new("Avatar", "A user profile picture component", "Data Display"), + new("Table", "A data table with sorting and pagination", "Data Display"), + new("Tabs", "A set of layered sections of content", "Navigation"), + new("Accordion", "A vertically stacked set of collapsible content", "Layout"), + new("DatePicker", "A date selection component", "Form"), + new("TimePicker", "A time selection component", "Form"), + new("Select", "A dropdown selection component", "Form"), + new("Checkbox", "A checkbox input component", "Form"), + new("Switch", "A toggle switch component", "Form"), + new("Slider", "A range slider input component", "Form"), + new("Progress", "A progress indicator component", "Feedback"), + new("Skeleton", "A loading placeholder component", "Feedback"), + new("Toast", "A temporary notification message", "Feedback"), + new("Tooltip", "A contextual help text component", "Overlay") + }; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JS.InvokeVoidAsync("eval", @" + document.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + // This will be handled by the component + } + }); + "); + } + } + + private async Task OpenSearch() + { + _isOpen = true; + SearchQuery = ""; + StateHasChanged(); + + // Focus the input after the modal is rendered + await Task.Delay(50); + try + { + await _searchInput.FocusAsync(); + } + catch + { + // Ignore focus errors + } + } + + private async Task CloseSearch() + { + _isOpen = false; + SearchQuery = ""; + StateHasChanged(); + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Escape") + { + await CloseSearch(); + } + else if (e.Key == "Enter") + { + var results = GetSearchResults(); + if (results.Any()) + { + await SelectResult(results.First()); + } + } + } + + private async Task SelectResult(SearchResult result) + { + await OnComponentSelected.InvokeAsync(result.Name); + await CloseSearch(); + } + + private IEnumerable GetSearchResults() + { + if (string.IsNullOrEmpty(SearchQuery)) + return Enumerable.Empty(); + + return _components.Where(c => + c.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || + c.Description.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || + c.Category.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) + ).Take(10); + } + + private record SearchResult(string Name, string Description, string Category); +} \ No newline at end of file From 6f1ce7c1c37f48195c9b7e37e2f293d91f1f46ef Mon Sep 17 00:00:00 2001 From: Shewart Date: Thu, 16 Oct 2025 23:10:20 +0200 Subject: [PATCH 39/45] feat: Enhance app.css with new gradient utilities, additional spacing and padding classes, and improved color variables for better styling flexibility and responsiveness --- NET9/BlazorInteractiveServer/wwwroot/app.css | 129 +++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index 0acdbda..0007109 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -13,6 +13,15 @@ --tw-space-y-reverse: 0; --tw-space-x-reverse: 0; --tw-border-style: solid; + --tw-gradient-position: initial; + --tw-gradient-from: #0000; + --tw-gradient-via: #0000; + --tw-gradient-to: #0000; + --tw-gradient-stops: initial; + --tw-gradient-via-stops: initial; + --tw-gradient-from-position: 0%; + --tw-gradient-via-position: 50%; + --tw-gradient-to-position: 100%; --tw-leading: initial; --tw-font-weight: initial; --tw-tracking: initial; @@ -79,12 +88,15 @@ --color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-700: oklch(48.8% 0.243 264.376); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-800: oklch(27.8% 0.033 256.848); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; --container-xs: 20rem; --container-md: 28rem; --container-lg: 32rem; + --container-2xl: 42rem; --container-4xl: 56rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); @@ -352,6 +364,9 @@ .mx-2 { margin-inline: calc(var(--spacing) * 2); } + .mx-4 { + margin-inline: calc(var(--spacing) * 4); + } .mx-auto { margin-inline: auto; } @@ -505,6 +520,9 @@ .max-h-60 { max-height: calc(var(--spacing) * 60); } + .max-h-96 { + max-height: calc(var(--spacing) * 96); + } .max-h-screen { max-height: 100vh; } @@ -565,12 +583,18 @@ .w-72 { width: calc(var(--spacing) * 72); } + .w-80 { + width: calc(var(--spacing) * 80); + } .w-\[1px\] { width: 1px; } .w-full { width: 100%; } + .max-w-2xl { + max-width: var(--container-2xl); + } .max-w-4xl { max-width: var(--container-4xl); } @@ -660,6 +684,9 @@ .items-center { align-items: center; } + .items-start { + align-items: flex-start; + } .justify-between { justify-content: space-between; } @@ -846,6 +873,9 @@ .bg-background { background-color: var(--background); } + .bg-background\/80 { + background-color: color-mix(in oklab, var(--background) 80%, transparent); + } .bg-background\/95 { background-color: color-mix(in oklab, var(--background) 95%, transparent); } @@ -879,6 +909,9 @@ .bg-muted { background-color: var(--muted); } + .bg-muted\/20 { + background-color: color-mix(in oklab, var(--muted) 20%, transparent); + } .bg-muted\/50 { background-color: color-mix(in oklab, var(--muted) 50%, transparent); } @@ -888,6 +921,9 @@ .bg-primary { background-color: var(--primary); } + .bg-primary\/10 { + background-color: color-mix(in oklab, var(--primary) 10%, transparent); + } .bg-primary\/20 { background-color: color-mix(in oklab, var(--primary) 20%, transparent); } @@ -900,6 +936,18 @@ .bg-yellow-500 { background-color: var(--color-yellow-500); } + .bg-gradient-to-br { + --tw-gradient-position: to bottom right in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .from-white { + --tw-gradient-from: var(--color-white); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-gray-400 { + --tw-gradient-to: var(--color-gray-400); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } .fill-primary-foreground { fill: var(--primary-foreground); } @@ -924,6 +972,12 @@ .p-6 { padding: calc(var(--spacing) * 6); } + .px-1 { + padding-inline: calc(var(--spacing) * 1); + } + .px-1\.5 { + padding-inline: calc(var(--spacing) * 1.5); + } .px-2 { padding-inline: calc(var(--spacing) * 2); } @@ -954,15 +1008,24 @@ .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } .py-4 { padding-block: calc(var(--spacing) * 4); } .py-6 { padding-block: calc(var(--spacing) * 6); } + .py-8 { + padding-block: calc(var(--spacing) * 8); + } .pt-0 { padding-top: calc(var(--spacing) * 0); } + .pt-\[15vh\] { + padding-top: 15vh; + } .pr-8 { padding-right: calc(var(--spacing) * 8); } @@ -1050,6 +1113,9 @@ .text-foreground { color: var(--foreground); } + .text-gray-800 { + color: var(--color-gray-800); + } .text-green-700 { color: var(--color-green-700); } @@ -1217,6 +1283,12 @@ color: var(--muted-foreground); } } + .last\:border-b-0 { + &:last-child { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 0px; + } + } .hover\:bg-accent { &:hover { @media (hover: hover) { @@ -1429,6 +1501,11 @@ background-color: color-mix(in oklab, var(--background) 60%, transparent); } } + .sm\:block { + @media (width >= 40rem) { + display: block; + } + } .sm\:inline-flex { @media (width >= 40rem) { display: inline-flex; @@ -1522,6 +1599,16 @@ line-height: var(--tw-leading, var(--text-4xl--line-height)); } } + .lg\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .lg\:hidden { + @media (width >= 64rem) { + display: none; + } + } .dark\:border-blue-500 { @media (prefers-color-scheme: dark) { border-color: var(--color-blue-500); @@ -1861,6 +1948,48 @@ inherits: false; initial-value: solid; } +@property --tw-gradient-position { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-via { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-to { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-via-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from-position { + syntax: ""; + inherits: false; + initial-value: 0%; +} +@property --tw-gradient-via-position { + syntax: ""; + inherits: false; + initial-value: 50%; +} +@property --tw-gradient-to-position { + syntax: ""; + inherits: false; + initial-value: 100%; +} @property --tw-leading { syntax: "*"; inherits: false; From cb2991480a662155a87ea0375a002fb38daa7ee1 Mon Sep 17 00:00:00 2001 From: Shewart Date: Sat, 18 Oct 2025 00:17:58 +0200 Subject: [PATCH 40/45] style: Update MainLayout navigation links with improved spacing, padding, and hover effects for enhanced user experience and visual consistency --- .../Components/Layout/MainLayout.razor | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index 8ef7b18..2fe1afc 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -21,11 +21,11 @@
    - -
    + }
    - @Body + @Body