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/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj index 79b14f8..b793beb 100644 --- a/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj +++ b/NET9/BlazorInteractiveServer/BlazorInteractiveServer.csproj @@ -6,8 +6,11 @@ enable - - - + + + + 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 diff --git a/NET9/BlazorInteractiveServer/Components/App.razor b/NET9/BlazorInteractiveServer/Components/App.razor index f21df6d..4330148 100644 --- a/NET9/BlazorInteractiveServer/Components/App.razor +++ b/NET9/BlazorInteractiveServer/Components/App.razor @@ -1,19 +1,50 @@  - + + + - - + + + + - + diff --git a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor index 13ec0ff..2fe1afc 100644 --- a/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor +++ b/NET9/BlazorInteractiveServer/Components/Layout/MainLayout.razor @@ -1,26 +1,154 @@ @inherits LayoutComponentBase -@inject Initialization init -
-
-

Brand Name

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

ShellUI

+
- + + + + +
+ + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + @if (_isMobileMenuOpen) + { +
+ +
+ + +
+ +
+
+
+ + + +
+

Menu

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

Hello, world!

- + + +
+
+ +
+ +
+

Variants

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

Sizes

+
+ + + + +
+
+ + +
+

States

+
+ + + +
+
+ + +
+

Form 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/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor new file mode 100644 index 0000000..3ed11fa --- /dev/null +++ b/NET9/BlazorInteractiveServer/Components/UI/ThemeToggle.razor @@ -0,0 +1,94 @@ +@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; } + + protected override async Task OnInitializedAsync() + { + _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() + { + _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/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor b/NET9/BlazorInteractiveServer/Components/UI/TimePicker.razor new file mode 100644 index 0000000..170bae8 --- /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/NET9/BlazorInteractiveServer/Components/_Imports.razor b/NET9/BlazorInteractiveServer/Components/_Imports.razor index 2f61f4b..a7cb58e 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 BlazorInteractiveServer.Components.UI 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/shellui.json b/NET9/BlazorInteractiveServer/shellui.json new file mode 100644 index 0000000..ef941db --- /dev/null +++ b/NET9/BlazorInteractiveServer/shellui.json @@ -0,0 +1,272 @@ +{ + "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": "theme-toggle", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:11:02.8329485Z", + "IsCustomized": false + }, + { + "Name": "input", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:37:49.7819622Z", + "IsCustomized": false + }, + { + "Name": "label", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:13.872833Z", + "IsCustomized": false + }, + { + "Name": "card", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.0379044Z", + "IsCustomized": false + }, + { + "Name": "alert", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.0701798Z", + "IsCustomized": false + }, + { + "Name": "badge", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.0964396Z", + "IsCustomized": false + }, + { + "Name": "textarea", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.1149473Z", + "IsCustomized": false + }, + { + "Name": "dialog", + "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 +} \ No newline at end of file diff --git a/NET9/BlazorInteractiveServer/tailwind.config.js b/NET9/BlazorInteractiveServer/tailwind.config.js new file mode 100644 index 0000000..e3e016d --- /dev/null +++ b/NET9/BlazorInteractiveServer/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './Components/**/*.{razor,html,cshtml}', + './Pages/**/*.{razor,html,cshtml}', + ], + darkMode: 'class', +} diff --git a/NET9/BlazorInteractiveServer/wwwroot/app.css b/NET9/BlazorInteractiveServer/wwwroot/app.css index 0ca86a1..5e31691 100644 --- a/NET9/BlazorInteractiveServer/wwwroot/app.css +++ b/NET9/BlazorInteractiveServer/wwwroot/app.css @@ -1,46 +1,2140 @@ -* { +/*! 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-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-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; + --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; + --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-duration: initial; + } + } +} +@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-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); + --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-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-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --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; + --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); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; margin: 0; padding: 0; - box-sizing: border-box -} - -.container { - max-width: 1400px; - margin: auto; - padding: 1rem -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid #e50000; -} - -.validation-message { - color: #e50000; -} - -.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; -} - -.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 + 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; + } +} +@layer utilities { + .pointer-events-auto { + pointer-events: auto; + } + .pointer-events-none { + pointer-events: none; + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .sticky { + position: sticky; + } + .inset-0 { + inset: 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); + } + .left-1\/2 { + left: calc(1/2 * 100%); + } + .left-full { + left: 100%; + } + .z-40 { + z-index: 40; + } + .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-2 { + margin-inline: calc(var(--spacing) * 2); + } + .mx-auto { + margin-inline: 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); + } + .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-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-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .mb-3 { + margin-bottom: calc(var(--spacing) * 3); + } + .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; + } + .grid { + display: grid; + } + .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); + } + .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); + } + .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); + } + .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-12 { + width: calc(var(--spacing) * 12); + } + .w-16 { + width: calc(var(--spacing) * 16); + } + .w-56 { + width: calc(var(--spacing) * 56); + } + .w-64 { + width: calc(var(--spacing) * 64); + } + .w-72 { + width: calc(var(--spacing) * 72); + } + .w-80 { + width: calc(var(--spacing) * 80); + } + .w-\[1px\] { + width: 1px; + } + .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); + } + .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-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .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-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-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; + 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-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; + 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))); + } + } + .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; + } + .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-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; + } + .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 { + 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)) { + border-color: color-mix(in oklab, var(--color-green-500) 50%, transparent); + } + } + .border-input { + border-color: var(--input); + } + .border-primary { + border-color: var(--primary); + } + .border-ring { + border-color: var(--ring); + } + .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-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); + } + .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-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); + } + .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); + } + .object-cover { + object-fit: cover; + } + .p-0\.5 { + padding: calc(var(--spacing) * 0.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); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .px-1\.5 { + padding-inline: calc(var(--spacing) * 1.5); + } + .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-5 { + padding-inline: calc(var(--spacing) * 5); + } + .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-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } + .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); + } + .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)); + } + .text-sm { + 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)); + } + .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-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); + } + .tracking-tight { + --tw-tracking: var(--tracking-tight); + letter-spacing: var(--tracking-tight); + } + .break-words { + overflow-wrap: break-word; + } + .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-gray-800 { + color: var(--color-gray-800); + } + .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); + } + .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-50 { + opacity: 50%; + } + .opacity-70 { + opacity: 70%; + } + .opacity-75 { + opacity: 75%; + } + .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); + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .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 { + --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-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .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)); + } + .transition-transform { + transition-property: transform, translate, scale, rotate; + 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; + } + } + .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-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) { + 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\: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) { + text-decoration-line: underline; + } + } + } + .hover\:opacity-100 { + &:hover { + @media (hover: hover) { + opacity: 100%; + } + } + } + .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); + 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\:ring-offset-background { + &:focus-visible { + --tw-ring-offset-color: var(--background); + } + } + .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%; + } + } + .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\:block { + @media (width >= 40rem) { + display: block; + } + } + .sm\:inline-flex { + @media (width >= 40rem) { + display: inline-flex; + } + } + .sm\:w-auto { + @media (width >= 40rem) { + width: auto; + } + } + .sm\:flex-col { + @media (width >= 40rem) { + flex-direction: column; + } + } + .sm\:flex-row { + @media (width >= 40rem) { + flex-direction: row; + } + } + .sm\:justify-end { + @media (width >= 40rem) { + 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)) { + --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\:p-6 { + @media (width >= 40rem) { + padding: calc(var(--spacing) * 6); + } + } + .sm\:text-left { + @media (width >= 40rem) { + text-align: left; + } + } + .sm\:text-3xl { + @media (width >= 40rem) { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + } + .sm\:text-base { + @media (width >= 40rem) { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + } + .sm\:text-xl { + @media (width >= 40rem) { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + } + .md\:inline-flex { + @media (width >= 48rem) { + display: inline-flex; + } + } + .md\:max-w-\[420px\] { + @media (width >= 48rem) { + max-width: 420px; + } + } + .md\:grid-cols-2 { + @media (width >= 48rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .md\:p-8 { + @media (width >= 48rem) { + padding: calc(var(--spacing) * 8); + } + } + .md\:text-4xl { + @media (width >= 48rem) { + font-size: var(--text-4xl); + 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); + } + } + .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\: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); + } + } + .dark\:text-yellow-400 { + @media (prefers-color-scheme: dark) { + 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; + } + } + .\[\&\>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); +} +.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-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; + initial-value: rotateX(0); +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; + initial-value: rotateY(0); +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; + initial-value: rotateZ(0); +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; + initial-value: skewX(0); +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; + initial-value: skewY(0); +} +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + 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; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@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; +} +@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-duration { + syntax: "*"; + inherits: false; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@keyframes pulse { + 50% { + opacity: 0.5; + } +} diff --git a/NET9/BlazorInteractiveServer/wwwroot/input.css b/NET9/BlazorInteractiveServer/wwwroot/input.css new file mode 100644 index 0000000..688a22a --- /dev/null +++ b/NET9/BlazorInteractiveServer/wwwroot/input.css @@ -0,0 +1,80 @@ +@import "tailwindcss"; + +@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; + } +} diff --git a/NET9/BlazorInteractiveServer/wwwroot/shellui.json b/NET9/BlazorInteractiveServer/wwwroot/shellui.json new file mode 100644 index 0000000..494cd48 --- /dev/null +++ b/NET9/BlazorInteractiveServer/wwwroot/shellui.json @@ -0,0 +1,62 @@ +{ + "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": "theme-toggle", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:11:02.8329485Z", + "IsCustomized": false + }, + { + "Name": "input", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:37:49.7819622Z", + "IsCustomized": false + }, + { + "Name": "label", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:13.872833Z", + "IsCustomized": false + }, + { + "Name": "card", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.0379044Z", + "IsCustomized": false + }, + { + "Name": "alert", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.0701798Z", + "IsCustomized": false + }, + { + "Name": "badge", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.0964396Z", + "IsCustomized": false + }, + { + "Name": "textarea", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.1149473Z", + "IsCustomized": false + }, + { + "Name": "dialog", + "Version": "0.1.0", + "InstalledAt": "2025-10-13T22:40:14.1393921Z", + "IsCustomized": false + } + ], + "ProjectType": 1 +} \ No newline at end of file diff --git a/SHELLDOCS_LAYOUT_GUIDE.md b/SHELLDOCS_LAYOUT_GUIDE.md new file mode 100644 index 0000000..d938006 --- /dev/null +++ b/SHELLDOCS_LAYOUT_GUIDE.md @@ -0,0 +1,1802 @@ +# ShellDocs Layout Structure - Complete Implementation Guide + +**Build fumadocs-style layouts in Blazor with Tailwind CSS** + +This guide provides complete implementation details for building the ShellDocs layout system that matches fumadocs' design and functionality. + +--- + +## 📋 Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [File Structure](#file-structure) +3. [Core Services & State Management](#core-services--state-management) +4. [Layout Components](#layout-components) +5. [Sidebar Components](#sidebar-components) +6. [Navigation Components](#navigation-components) +7. [CSS & Styling](#css--styling) +8. [JavaScript Interop](#javascript-interop) +9. [Configuration](#configuration) +10. [Usage Examples](#usage-examples) + +--- + +## 📐 Architecture Overview + +### Layout Structure Hierarchy + +``` +DocsLayout (Main Container) +├── NavProvider (State: transparent mode) +├── SidebarProvider (State: open, collapsed) +│ ├── Sidebar (Desktop) +│ │ ├── SidebarHeader (Logo, search, actions) +│ │ ├── SidebarViewport (Scrollable content) +│ │ │ └── SidebarPageTree (Navigation items) +│ │ └── SidebarFooter (Theme toggle, links) +│ │ +│ ├── SidebarMobile (Mobile drawer) +│ │ └── Same structure as Sidebar +│ │ +│ └── Navbar (Mobile only, fixed top) +│ ├── Logo +│ ├── Search toggle +│ └── Sidebar toggle +│ +├── LayoutBody (Main content area) +│ └── @Body (Page content) +│ +└── TableOfContents (Fixed right, desktop only) +``` + +### Responsive Behavior + +| Breakpoint | Sidebar | Navbar | TOC | +|------------|---------|--------|-----| +| Desktop (xl+) | Fixed left, collapsible | Hidden | Fixed right | +| Tablet (md-xl) | Fixed left, collapsible | Hidden | Popover | +| Mobile ( +/// Manages sidebar state (open/closed, collapsed/expanded) +/// +public class SidebarService +{ + private bool _isOpen = false; + private bool _isCollapsed = false; + + /// + /// Sidebar open state (mobile) + /// + public bool IsOpen + { + get => _isOpen; + set + { + if (_isOpen != value) + { + _isOpen = value; + OnStateChanged?.Invoke(); + } + } + } + + /// + /// Sidebar collapsed state (desktop) + /// + public bool IsCollapsed + { + get => _isCollapsed; + set + { + if (_isCollapsed != value) + { + _isCollapsed = value; + OnStateChanged?.Invoke(); + } + } + } + + public event Action? OnStateChanged; + + public void Toggle() => IsOpen = !IsOpen; + public void ToggleCollapsed() => IsCollapsed = !IsCollapsed; + public void Open() => IsOpen = true; + public void Close() => IsOpen = false; + public void Collapse() => IsCollapsed = true; + public void Expand() => IsCollapsed = false; +} +``` + +### 2. NavigationService.cs + +```csharp +using System; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; + +namespace ShellDocs.Blazor.Services; + +/// +/// Manages navigation state and transparency mode +/// +public class NavigationService : IDisposable +{ + private readonly NavigationManager _navigationManager; + private bool _isTransparent = false; + + public NavigationService(NavigationManager navigationManager) + { + _navigationManager = navigationManager; + _navigationManager.LocationChanged += OnLocationChanged; + } + + public bool IsTransparent + { + get => _isTransparent; + set + { + if (_isTransparent != value) + { + _isTransparent = value; + OnStateChanged?.Invoke(); + } + } + } + + public event Action? OnStateChanged; + + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + // Reset transparency on navigation + IsTransparent = false; + OnStateChanged?.Invoke(); + } + + public void Dispose() + { + _navigationManager.LocationChanged -= OnLocationChanged; + } +} +``` + +### 3. ThemeService.cs + +```csharp +using System; +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace ShellDocs.Blazor.Services; + +/// +/// Manages theme state (dark/light) with persistence +/// +public class ThemeService +{ + private readonly IJSRuntime _js; + private string _theme = "light"; + + public ThemeService(IJSRuntime js) + { + _js = js; + } + + public string Theme + { + get => _theme; + private set + { + if (_theme != value) + { + _theme = value; + OnThemeChanged?.Invoke(); + } + } + } + + public event Action? OnThemeChanged; + + /// + /// Initialize theme from localStorage or system preference + /// + public async Task InitializeAsync() + { + try + { + var theme = await _js.InvokeAsync("ShellDocs.Theme.getTheme"); + Theme = theme; + } + catch + { + Theme = "light"; + } + } + + /// + /// Toggle between light and dark mode + /// + public async Task ToggleAsync() + { + var newTheme = Theme == "dark" ? "light" : "dark"; + await SetThemeAsync(newTheme); + } + + /// + /// Set specific theme + /// + public async Task SetThemeAsync(string theme) + { + Theme = theme; + await _js.InvokeVoidAsync("ShellDocs.Theme.setTheme", theme); + } +} +``` + +### 4. Service Registration (Program.cs) + +```csharp +// Add to Program.cs +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +--- + +## 📦 Layout Components + +### 1. DocsLayout.razor + +```razor +@inherits LayoutComponentBase +@inject SidebarService SidebarService +@inject NavigationService NavigationService +@implements IDisposable + + +
+ + @* Desktop Sidebar *@ + + + @* Mobile Sidebar (Drawer) *@ + + + @* Mobile Navbar *@ + + + @* Main Content Area *@ +
+ @* Optional: Top-level tabs *@ + @if (Tabs != null && Tabs.Any()) + { + + } + + @* Main content *@ +
+ @Body +
+
+ + @* Desktop Table of Contents *@ + @if (ShowTableOfContents) + { + + } +
+
+ +@code { + [Parameter] public PageTree PageTree { get; set; } = null!; + [Parameter] public string Title { get; set; } = "Documentation"; + [Parameter] public string? Logo { get; set; } + [Parameter] public List Links { get; set; } = new(); + [Parameter] public List TocItems { get; set; } = new(); + [Parameter] public List? Tabs { get; set; } + [Parameter] public bool CollapsibleControl { get; set; } = true; + [Parameter] public bool SearchEnabled { get; set; } = true; + [Parameter] public bool ThemeToggleEnabled { get; set; } = true; + [Parameter] public bool ShowTableOfContents { get; set; } = true; + + protected override void OnInitialized() + { + SidebarService.OnStateChanged += StateHasChanged; + NavigationService.OnStateChanged += StateHasChanged; + } + + private string GetContentStyles() + { + if (SidebarService.IsCollapsed) + { + return "padding-inline-start: min(calc(100vw - var(--sd-page-width)), var(--sd-sidebar-width));"; + } + return "padding-inline-start: var(--sd-sidebar-width);"; + } + + public void Dispose() + { + SidebarService.OnStateChanged -= StateHasChanged; + NavigationService.OnStateChanged -= StateHasChanged; + } +} +``` + +--- + +## 🗂️ Sidebar Components + +### 1. Sidebar.razor (Desktop) + +```razor +@inject SidebarService SidebarService +@inject IJSRuntime JS + + + +@code { + [Parameter] public PageTree PageTree { get; set; } = null!; + [Parameter] public string Title { get; set; } = "Docs"; + [Parameter] public string? Logo { get; set; } + [Parameter] public List Links { get; set; } = new(); + [Parameter] public bool CollapsibleControl { get; set; } = true; + [Parameter] public bool SearchEnabled { get; set; } = true; + [Parameter] public bool ThemeToggleEnabled { get; set; } = true; + + private bool _isHovering = false; + private System.Timers.Timer? _hoverTimer; + + protected override void OnInitialized() + { + SidebarService.OnStateChanged += StateHasChanged; + } + + private void OnMouseEnter(MouseEventArgs e) + { + if (!SidebarService.IsCollapsed) return; + + _hoverTimer?.Stop(); + _isHovering = true; + StateHasChanged(); + } + + private void OnMouseLeave(MouseEventArgs e) + { + if (!SidebarService.IsCollapsed) return; + + _hoverTimer = new System.Timers.Timer(500); + _hoverTimer.Elapsed += (s, e) => + { + _isHovering = false; + InvokeAsync(StateHasChanged); + _hoverTimer?.Dispose(); + }; + _hoverTimer.Start(); + } + + private string GetSidebarStyles() + { + var styles = new List(); + + if (SidebarService.IsCollapsed) + { + var offset = _isHovering + ? "calc(var(--spacing) * 2)" + : "calc(16px - 100%)"; + styles.Add($"--sd-sidebar-offset: {offset}"); + styles.Add("--sd-sidebar-margin: 0.5rem"); + } + else + { + styles.Add("--sd-sidebar-margin: 0px"); + } + + return string.Join("; ", styles); + } + + public void Dispose() + { + SidebarService.OnStateChanged -= StateHasChanged; + _hoverTimer?.Dispose(); + } +} +``` + +### 2. SidebarMobile.razor (Mobile Drawer) + +```razor +@inject SidebarService SidebarService +@inject IJSRuntime JS + +@* Overlay backdrop *@ +@if (SidebarService.IsOpen) +{ + +} + +@* Mobile Sidebar Drawer *@ + + +@code { + [Parameter] public PageTree PageTree { get; set; } = null!; + [Parameter] public string Title { get; set; } = "Docs"; + [Parameter] public string? Logo { get; set; } + [Parameter] public List Links { get; set; } = new(); + [Parameter] public bool SearchEnabled { get; set; } = true; + [Parameter] public bool ThemeToggleEnabled { get; set; } = true; + + protected override void OnInitialized() + { + SidebarService.OnStateChanged += StateHasChanged; + } + + private void Close() + { + SidebarService.Close(); + } + + public void Dispose() + { + SidebarService.OnStateChanged -= StateHasChanged; + } +} +``` + +### 3. SidebarHeader.razor + +```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; + [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!** 🚀 + diff --git a/src/ShellUI.CLI/Program.cs b/src/ShellUI.CLI/Program.cs index 3751555..10a72f2 100644 --- a/src/ShellUI.CLI/Program.cs +++ b/src/ShellUI.CLI/Program.cs @@ -1,2 +1,178 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using System.CommandLine; +using Spectre.Console; +using ShellUI.Templates; +using ShellUI.Core.Models; +using ShellUI.CLI.Services; + +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(async (force, style) => + { + try + { + AnsiConsole.Write(new FigletText("ShellUI").Color(Color.Blue)); + await InitService.InitializeAsync(style, force); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); + } + }, 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) => + { + try + { + ComponentInstaller.InstallComponents(components, force); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); + } + }, 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) => + { + try + { + ComponentManager.ListComponents(installed, available); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); + } + }, 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) => + { + try + { + ComponentManager.RemoveComponents(components); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); + } + }, 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) => + { + try + { + ComponentManager.UpdateComponents(components, all); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); + } + }, componentsArg, allOption); + + return command; + } +} diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs new file mode 100644 index 0000000..d8b1fc6 --- /dev/null +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -0,0 +1,172 @@ +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)}[/]"); + } + + 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)) + { + 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 + } +} + 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}"); + } + } +} + diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs new file mode 100644 index 0000000..fd0d152 --- /dev/null +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -0,0 +1,196 @@ +using ShellUI.Core.Models; +using ShellUI.Templates; +using System.Text.Json; +using Spectre.Console; + +namespace ShellUI.CLI.Services; + +public class InitService +{ + public static async Task InitializeAsync(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; + } + + // 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)) + { + 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); + } + } +} + 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; } +} + 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); + } +} + diff --git a/src/ShellUI.CLI/ShellUI.CLI.csproj b/src/ShellUI.CLI/ShellUI.CLI.csproj index 96b518a..b26b941 100644 --- a/src/ShellUI.CLI/ShellUI.CLI.csproj +++ b/src/ShellUI.CLI/ShellUI.CLI.csproj @@ -15,6 +15,22 @@ 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/shelltechlabs/shellui + https://github.com/shelltechlabs/shellui + MIT + + + + 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/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/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/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/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/Button.razor b/src/ShellUI.Components/Components/Button.razor new file mode 100644 index 0000000..fa9fca9 --- /dev/null +++ b/src/ShellUI.Components/Components/Button.razor @@ -0,0 +1,52 @@ +@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 async Task HandleClick(MouseEventArgs args) + { + if (!Disabled && !IsLoading) + { + await OnClick.InvokeAsync(args); + } + } +} + 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/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/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/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/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/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/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/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/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 new file mode 100644 index 0000000..abe9aae --- /dev/null +++ b/src/ShellUI.Components/Components/ThemeToggle.razor @@ -0,0 +1,99 @@ +@namespace ShellUI.Components +@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; } + + protected override async Task OnInitializedAsync() + { + _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() + { + _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.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" + }; +} + diff --git a/src/ShellUI.Components/README.md b/src/ShellUI.Components/README.md new file mode 100644 index 0000000..da23832 --- /dev/null +++ b/src/ShellUI.Components/README.md @@ -0,0 +1,227 @@ +# ShellUI Components + +Beautiful, accessible Blazor components inspired by shadcn/ui. A CLI-first component library with Tailwind CSS styling. + +## Features + +- 🎨 **53+ Production-ready components** - Button, Input, Card, Dialog, Table, and more +- 🎯 **CLI-first approach** - Install components individually with `dotnet shellui add` +- 🎨 **Tailwind CSS styling** - Utility-first CSS with dark mode support +- ♿ **Accessible by default** - Built with accessibility in mind +- 📱 **Responsive design** - Mobile-first approach +- 🔧 **Fully customizable** - Copy components to your project for full control + +## Quick Start + +### Option 1: CLI Tool (Recommended) + +The CLI tool provides the best developer experience with automatic setup: + +#### 1. Install the CLI tool + +```bash +dotnet tool install -g ShellUI.CLI +``` + +#### 2. Initialize ShellUI in your project + +```bash +dotnet shellui init +``` + +This automatically: +- ✅ Downloads Tailwind CSS CLI (standalone, no Node.js required) +- ✅ Creates CSS files and configuration +- ✅ Sets up MSBuild integration for auto-building +- ✅ Creates component folders + +#### 3. Add components + +```bash +# Add a button component +dotnet shellui add button + +# Add multiple components +dotnet shellui add input card dialog + +# List available components +dotnet shellui list +``` + +### Option 2: NuGet Package + +For manual setup or existing projects: + +#### 1. Install the package + +```bash +dotnet add package ShellUI.Components +``` + +#### 2. Set up Tailwind CSS + +Choose one of these methods: + +**Method A: Tailwind CLI (Recommended)** +```bash +# Download Tailwind CLI (standalone) +curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-windows-x64.exe +# Or for Linux/Mac: tailwindcss-linux-x64 or tailwindcss-macos-x64 + +# Create input.css +echo '@tailwind base; +@tailwind components; +@tailwind utilities;' > wwwroot/input.css + +# Create tailwind.config.js +echo 'module.exports = { + content: ["./**/*.{razor,html,cs}"], + theme: { extend: {} }, + plugins: [] +}' > tailwind.config.js + +# Build CSS +./tailwindcss -i wwwroot/input.css -o wwwroot/app.css +``` + +**Method B: npm (if you prefer Node.js)** +```bash +# Install Tailwind CSS +npm install -D tailwindcss +npx tailwindcss init + +# Update tailwind.config.js +echo 'module.exports = { + content: ["./**/*.{razor,html,cs}"], + theme: { extend: {} }, + plugins: [] +}' > tailwind.config.js + +# Create input.css +echo '@tailwind base; +@tailwind components; +@tailwind utilities;' > wwwroot/input.css + +# Build CSS +npx tailwindcss -i wwwroot/input.css -o wwwroot/app.css +``` + +#### 3. Add to your layout + +```html + + +``` + +#### 4. Use components + +```html +@using ShellUI.Components + + + + + + Hello World + + +

    This is a card component!

    +
    +
    +``` + +## Available Components + +### Form Components +- **Button** - Various variants and sizes +- **Input** - Text input with validation +- **Textarea** - Multi-line text input +- **Select** - Dropdown selection +- **Checkbox** - Checkbox input +- **Switch** - Toggle switch +- **RadioGroup** - Radio button groups +- **Slider** - Range slider +- **Combobox** - Searchable dropdown +- **DatePicker** - Date selection +- **TimePicker** - Time selection +- **DateRangePicker** - Date range selection +- **InputOTP** - One-time password input +- **Form** - Form wrapper with validation + +### Layout Components +- **Card** - Content container +- **Dialog** - Modal dialog +- **Sheet** - Side panel +- **Drawer** - Mobile drawer +- **Popover** - Floating content +- **Tooltip** - Hover tooltip +- **Separator** - Visual divider +- **ScrollArea** - Custom scrollable area +- **Resizable** - Resizable panels +- **Collapsible** - Collapsible content + +### Navigation Components +- **Navbar** - Top navigation bar +- **Sidebar** - Side navigation +- **NavigationMenu** - Navigation menu +- **Menubar** - Menu bar +- **Breadcrumb** - Breadcrumb navigation +- **Pagination** - Page navigation +- **Tabs** - Tab navigation + +### Data Display +- **Table** - Data table +- **Badge** - Status badges +- **Avatar** - User avatars +- **Alert** - Alert messages +- **Toast** - Toast notifications +- **Skeleton** - Loading placeholders +- **Progress** - Progress indicators + +### Interactive Components +- **Dropdown** - Dropdown menu +- **Accordion** - Collapsible sections +- **Toggle** - Toggle button +- **ThemeToggle** - Dark/light mode toggle + +## Bootstrap Compatibility + +ShellUI components work alongside Bootstrap. You can: + +- **Keep both** - ShellUI and Bootstrap can coexist +- **Remove Bootstrap** - Delete Bootstrap references if you prefer Tailwind-only +- **Gradual migration** - Use ShellUI for new components, keep Bootstrap for existing ones + +## Customization + +Components are copied to your project, giving you full control: + +```bash +# Components are installed to Components/UI/ +Components/ + UI/ + Button.razor + Input.razor + Card.razor + # ... other components +``` + +## Documentation + +- [Component Gallery](https://shellui.dev/components) +- [Installation Guide](https://shellui.dev/docs/installation) +- [Customization](https://shellui.dev/docs/customization) + +## Contributing + +We welcome contributions! Please see our [Contributing Guide](https://github.com/shelltechlabs/shellui/blob/main/CONTRIBUTING.md). + +## License + +MIT License - see [LICENSE](https://github.com/shelltechlabs/shellui/blob/main/LICENSE) for details. + +## Support + +- 📖 [Documentation](https://shellui.dev) +- 🐛 [Issues](https://github.com/shelltechlabs/shellui/issues) +- 💬 [Discussions](https://github.com/shelltechlabs/shellui/discussions) 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); + } +} + diff --git a/src/ShellUI.Components/ShellUI.Components.csproj b/src/ShellUI.Components/ShellUI.Components.csproj index fdd725a..a57b81b 100644 --- a/src/ShellUI.Components/ShellUI.Components.csproj +++ b/src/ShellUI.Components/ShellUI.Components.csproj @@ -4,6 +4,22 @@ net9.0 enable enable + + + ShellUI.Components + 1.0.0 + Shell Technologies + Shell Technologies + ShellUI + Beautiful, accessible Blazor components inspired by shadcn/ui. CLI-first component library with Tailwind CSS styling. + https://github.com/shelltechlabs/shellui + https://github.com/shelltechlabs/shellui + git + MIT + blazor;components;ui;tailwind;shadcn;shellui + true + true + snupkg @@ -19,4 +35,8 @@ + + + + diff --git a/src/ShellUI.Components/icon.svg b/src/ShellUI.Components/icon.svg new file mode 100644 index 0000000..19e1eaf --- /dev/null +++ b/src/ShellUI.Components/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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.Core/ShellUI.Core.csproj b/src/ShellUI.Core/ShellUI.Core.csproj index 125f4c9..beecfea 100644 --- a/src/ShellUI.Core/ShellUI.Core.csproj +++ b/src/ShellUI.Core/ShellUI.Core.csproj @@ -4,6 +4,22 @@ net9.0 enable enable + + + ShellUI.Core + 1.0.0 + Shell Technologies + Shell Technologies + ShellUI + Core models and interfaces for ShellUI component library. + https://github.com/shelltechlabs/shellui + https://github.com/shelltechlabs/shellui + git + MIT + blazor;components;ui;shellui;core + true + true + snupkg 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/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..0aaa206 --- /dev/null +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -0,0 +1,146 @@ +using ShellUI.Core.Models; +using ShellUI.Templates.Templates; + +namespace ShellUI.Templates; + +public static class ComponentRegistry +{ + public static readonly Dictionary Components = new() + { + { "button", ButtonTemplate.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 }, + { "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) + { + return componentName.ToLower() switch + { + "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, + "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 + }; + } + + 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/CssTemplates.cs b/src/ShellUI.Templates/CssTemplates.cs new file mode 100644 index 0000000..ea44ef3 --- /dev/null +++ b/src/ShellUI.Templates/CssTemplates.cs @@ -0,0 +1,107 @@ +namespace ShellUI.Templates; + +public static class CssTemplates +{ + public static string InputCss => @"@import ""tailwindcss""; + +@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 => @"/* + * 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', +} +"; +} 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/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/AlertTemplate.cs b/src/ShellUI.Templates/Templates/AlertTemplate.cs new file mode 100644 index 0000000..9304a77 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertTemplate.cs @@ -0,0 +1,63 @@ +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 + +
    svg]:text-destructive"" : """")""> + @if (!string.IsNullOrEmpty(Icon)) + { +
    +
    svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground""> + @((MarkupString)Icon) +
    +
    + @if (!string.IsNullOrEmpty(Title)) + { +
    @Title
    + } + @ChildContent +
    +
    + } + else + { + @if (!string.IsNullOrEmpty(Title)) + { +
    @Title
    + } +
    + @ChildContent +
    + } +
    + +@code { + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Icon { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Variant { get; set; } = ""default""; +} +"; +} + 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/BadgeTemplate.cs b/src/ShellUI.Templates/Templates/BadgeTemplate.cs new file mode 100644 index 0000000..0fb04e9 --- /dev/null +++ b/src/ShellUI.Templates/Templates/BadgeTemplate.cs @@ -0,0 +1,33 @@ +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; } +} +"; +} + 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/ButtonTemplate.cs b/src/ShellUI.Templates/Templates/ButtonTemplate.cs new file mode 100644 index 0000000..7028afa --- /dev/null +++ b/src/ShellUI.Templates/Templates/ButtonTemplate.cs @@ -0,0 +1,72 @@ +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 async Task HandleClick(MouseEventArgs args) + { + if (!Disabled && !IsLoading) + { + await OnClick.InvokeAsync(args); + } + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/CardTemplate.cs b/src/ShellUI.Templates/Templates/CardTemplate.cs new file mode 100644 index 0000000..2fbccea --- /dev/null +++ b/src/ShellUI.Templates/Templates/CardTemplate.cs @@ -0,0 +1,55 @@ +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 +
    + } + + @if (ChildContent != null) + { +
    + @ChildContent +
    + } + + @if (Footer != null) + { +
    + @Footer +
    + } +
    + +@code { + [Parameter] + public RenderFragment? Header { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public RenderFragment? Footer { 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/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/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/InputTemplate.cs b/src/ShellUI.Templates/Templates/InputTemplate.cs new file mode 100644 index 0000000..29a89e3 --- /dev/null +++ b/src/ShellUI.Templates/Templates/InputTemplate.cs @@ -0,0 +1,56 @@ +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 EventCallback ValueChanged { get; set; } + + [Parameter] + public string? Placeholder { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleInput(ChangeEventArgs e) + { + Value = e.Value?.ToString(); + await ValueChanged.InvokeAsync(Value); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/LabelTemplate.cs b/src/ShellUI.Templates/Templates/LabelTemplate.cs new file mode 100644 index 0000000..16859d0 --- /dev/null +++ b/src/ShellUI.Templates/Templates/LabelTemplate.cs @@ -0,0 +1,39 @@ +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; } +} +"; +} + 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/TextareaTemplate.cs b/src/ShellUI.Templates/Templates/TextareaTemplate.cs new file mode 100644 index 0000000..c8a8613 --- /dev/null +++ b/src/ShellUI.Templates/Templates/TextareaTemplate.cs @@ -0,0 +1,52 @@ +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 EventCallback ValueChanged { get; set; } + + [Parameter] + public string? Placeholder { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleInput(ChangeEventArgs 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 new file mode 100644 index 0000000..82e8fdf --- /dev/null +++ b/src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs @@ -0,0 +1,189 @@ +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 => @"@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; } + + protected override async Task OnInitializedAsync() + { + _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() + { + _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; + } +} +"; + + 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); + } +} +"; +} + diff --git a/src/ShellUI.Templates/Templates/TimePickerTemplate.cs b/src/ShellUI.Templates/Templates/TimePickerTemplate.cs new file mode 100644 index 0000000..bc85296 --- /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; +} +"; +} +