diff --git a/ShellUI.Tests/PackageBehaviorTests.cs b/ShellUI.Tests/PackageBehaviorTests.cs index 0b77dee..39dfce5 100644 --- a/ShellUI.Tests/PackageBehaviorTests.cs +++ b/ShellUI.Tests/PackageBehaviorTests.cs @@ -186,6 +186,29 @@ public void ScrollArea_SizesTheScrollingElement(string? height, string? maxHeigh Assert.Equal(expected, area.Find("div.overflow-auto").GetAttribute("style")); } + [Fact] + public void CompositionalStepper_ShowsTheActiveStepAfterNext() + { + using var ctx = new BunitContext(); + var changes = new List(); + + var stepper = ctx.Render(p => p + .Add(c => c.TotalSteps, 2) + .Add(c => c.CurrentStepChanged, EventCallback.Factory.Create(this, v => changes.Add(v))) + .AddChildContent(l => l + .AddChildContent(s => s.Add(c => c.Value, "0").Add(c => c.Title, "Account")) + .AddChildContent(s => s.Add(c => c.Value, "1").Add(c => c.Title, "Profile"))) + .AddChildContent(c => c.Add(x => x.Value, "0").AddChildContent("First")) + .AddChildContent(c => c.Add(x => x.Value, "1").AddChildContent("Second"))); + + Assert.Contains("First", stepper.Markup); + stepper.FindAll("button").Single(b => b.TextContent.Trim() == "Next").Click(); + + Assert.Equal(new[] { 1 }, changes); + Assert.Contains("Second", stepper.Markup); + Assert.DoesNotContain("First", stepper.Markup); + } + [Fact] public void TimePicker_ShowsASetValue() { diff --git a/ShellUI.Tests/TemplateCompileTests.cs b/ShellUI.Tests/TemplateCompileTests.cs index 0c09a16..362a787 100644 --- a/ShellUI.Tests/TemplateCompileTests.cs +++ b/ShellUI.Tests/TemplateCompileTests.cs @@ -16,6 +16,9 @@ public class TemplateCompileTests [InlineData("alert-variants")] [InlineData("badge-variants")] [InlineData("button-variants")] + [InlineData("stepper-models")] + [InlineData("input-variants")] + [InlineData("textarea-variants")] public void CsharpTemplate_GeneratedContentParses(string componentName) { var content = ComponentRegistry.GetComponentContent(componentName); diff --git a/ShellUI.Tests/TemplateSyncTests.cs b/ShellUI.Tests/TemplateSyncTests.cs index a687499..617a3d1 100644 --- a/ShellUI.Tests/TemplateSyncTests.cs +++ b/ShellUI.Tests/TemplateSyncTests.cs @@ -163,6 +163,13 @@ public class TemplateSyncTests [InlineData("time-picker", "TimePicker.razor")] [InlineData("date-picker", "DatePicker.razor")] [InlineData("navigation-menu-item", "NavigationMenuItem.razor")] + [InlineData("stepper", "Stepper.razor")] + [InlineData("stepper-step", "StepperStep.razor")] + [InlineData("stepper-content", "StepperContent.razor")] + [InlineData("input", "Input.razor")] + [InlineData("textarea", "Textarea.razor")] + [InlineData("sheet", "Sheet.razor")] + [InlineData("drawer", "Drawer.razor")] [InlineData("copy-button", "CopyButton.razor")] [InlineData("sidebar-provider", "SidebarProvider.razor")] [InlineData("app-sidebar", "AppSidebar.razor")] @@ -222,6 +229,13 @@ public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string raz [InlineData("time-picker", "TimePicker.razor")] [InlineData("date-picker", "DatePicker.razor")] [InlineData("navigation-menu-item", "NavigationMenuItem.razor")] + [InlineData("stepper", "Stepper.razor")] + [InlineData("stepper-step", "StepperStep.razor")] + [InlineData("stepper-content", "StepperContent.razor")] + [InlineData("input", "Input.razor")] + [InlineData("textarea", "Textarea.razor")] + [InlineData("sheet", "Sheet.razor")] + [InlineData("drawer", "Drawer.razor")] public void TemplateMarkup_MatchesLiveLibrary(string templateName, string razorFileName) { var liveContent = WithoutClassNameAlias(File.ReadAllText(GetLiveRazorPath(razorFileName))); diff --git a/src/ShellUI.Components/Components/Drawer.razor b/src/ShellUI.Components/Components/Drawer.razor index 6c01d76..999e497 100644 --- a/src/ShellUI.Components/Components/Drawer.razor +++ b/src/ShellUI.Components/Components/Drawer.razor @@ -42,7 +42,7 @@ else if (Open) [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } - // CLI templates need Compositional="true"; the package also switches when Title and Description are empty. + // Compositional mode also turns on when Title and Description are both empty. [Parameter] public bool Compositional { get; set; } private bool UseCompositional => Compositional || (string.IsNullOrEmpty(Title) && string.IsNullOrEmpty(Description)); diff --git a/src/ShellUI.Components/Components/Sheet.razor b/src/ShellUI.Components/Components/Sheet.razor index 5a46821..632b0c4 100644 --- a/src/ShellUI.Components/Components/Sheet.razor +++ b/src/ShellUI.Components/Components/Sheet.razor @@ -50,7 +50,7 @@ else if (Open) [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } - // CLI templates need Compositional="true"; the package also switches when Title and Description are empty. + // Compositional mode also turns on when Title and Description are both empty. [Parameter] public bool Compositional { get; set; } private bool UseCompositional => Compositional || (string.IsNullOrEmpty(Title) && string.IsNullOrEmpty(Description)); diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index 215d750..dc9687e 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -11,6 +11,8 @@ public static class ComponentRegistry { "shellui-js", ShellUIJsTemplate.Metadata }, { "button", ButtonTemplate.Metadata }, { "button-variants", ButtonVariantsTemplate.Metadata }, + { "input-variants", InputVariantsTemplate.Metadata }, + { "textarea-variants", TextareaVariantsTemplate.Metadata }, { "theme-toggle", ThemeToggleTemplate.Metadata }, { "input", InputTemplate.Metadata }, { "card", CardTemplate.Metadata }, @@ -250,6 +252,7 @@ public static class ComponentRegistry { "chat-input", ChatInputTemplate.Metadata }, { "chat", ChatTemplate.Metadata }, { "command-models", CommandModelsTemplate.Metadata }, + { "stepper-models", StepperModelsTemplate.Metadata }, }; public static string? GetComponentContent(string componentName) @@ -260,6 +263,8 @@ public static class ComponentRegistry "shellui-js" => ShellUIJsTemplate.Content, "button" => ButtonTemplate.Content, "button-variants" => ButtonVariantsTemplate.Content, + "input-variants" => InputVariantsTemplate.Content, + "textarea-variants" => TextareaVariantsTemplate.Content, "theme-toggle" => ThemeToggleTemplate.Content, "input" => InputTemplate.Content, "card" => CardTemplate.Content, @@ -499,6 +504,7 @@ public static class ComponentRegistry "chat-input" => ChatInputTemplate.Content, "chat" => ChatTemplate.Content, "command-models" => CommandModelsTemplate.Content, + "stepper-models" => StepperModelsTemplate.Content, _ => null }; } diff --git a/src/ShellUI.Templates/Templates/DrawerTemplate.cs b/src/ShellUI.Templates/Templates/DrawerTemplate.cs index d3bab8a..94d5d4c 100644 --- a/src/ShellUI.Templates/Templates/DrawerTemplate.cs +++ b/src/ShellUI.Templates/Templates/DrawerTemplate.cs @@ -16,12 +16,11 @@ public class DrawerTemplate }; public static string Content => @"@namespace YourProjectNamespace.Components.UI -@using YourProjectNamespace.Components.UI.Variants @using Microsoft.JSInterop @implements IAsyncDisposable @inject IJSRuntime JS -@if (Compositional) +@if (UseCompositional) { @ChildContent @@ -31,22 +30,22 @@ @inject IJSRuntime JS {
-
-
- @if (!string.IsNullOrEmpty(Title)) - { -

@Title

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

@Description

- } +
+
+ @if (!string.IsNullOrEmpty(Title)) + { +

@Title

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

@Description

+ } +
+
@ChildContent
+
+ +
-
@ChildContent
-
- -
-
} @code { @@ -57,10 +56,14 @@ @inject IJSRuntime JS [Parameter] public DrawerSide Side { get; set; } = DrawerSide.Bottom; [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public string? Class { get; set; } - [Parameter] public bool Compositional { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + // Compositional mode also turns on when Title and Description are both empty. + [Parameter] public bool Compositional { get; set; } + + private bool UseCompositional => Compositional || (string.IsNullOrEmpty(Title) && string.IsNullOrEmpty(Description)); + private async Task Close() { Open = false; @@ -85,7 +88,7 @@ private async Task OnPanelKeyDownAsync(KeyboardEventArgs e) protected override async Task OnAfterRenderAsync(bool firstRender) { - var panelOpen = Open && !Compositional; + var panelOpen = Open && !UseCompositional; if (panelOpen && !_focused) { _focused = true; diff --git a/src/ShellUI.Templates/Templates/InputTemplate.cs b/src/ShellUI.Templates/Templates/InputTemplate.cs index 1003eb0..3c70c61 100644 --- a/src/ShellUI.Templates/Templates/InputTemplate.cs +++ b/src/ShellUI.Templates/Templates/InputTemplate.cs @@ -12,12 +12,13 @@ public static class InputTemplate Category = ComponentCategory.Form, FilePath = "Input.razor", - Dependencies = new List() + Dependencies = new List { "input-variants" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using YourProjectNamespace.Components.UI.Variants - ""border-destructive focus-visible:ring-destructive"", - _ => """" - }; - - return Shell.Cn(baseClasses, variantClasses, className); - } - } } "; } diff --git a/src/ShellUI.Templates/Templates/InputVariantsTemplate.cs b/src/ShellUI.Templates/Templates/InputVariantsTemplate.cs new file mode 100644 index 0000000..181bded --- /dev/null +++ b/src/ShellUI.Templates/Templates/InputVariantsTemplate.cs @@ -0,0 +1,41 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class InputVariantsTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "input-variants", + DisplayName = "Input Variants", + Description = "Input variant enum and utility classes", + Category = ComponentCategory.Utility, + FilePath = "Variants/InputVariants.cs", + IsAvailable = false + }; + + public static string Content => @"namespace YourProjectNamespace.Components.UI.Variants; + +public enum InputVariant +{ + Default, + Error +} + +public static class InputVariants +{ + public static string Get(InputVariant variant = InputVariant.Default, string? className = null) + { + var baseClasses = ""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""; + + var variantClasses = variant switch + { + InputVariant.Error => ""border-destructive focus-visible:ring-destructive"", + _ => """" + }; + + return Shell.Cn(baseClasses, variantClasses, className); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SheetTemplate.cs b/src/ShellUI.Templates/Templates/SheetTemplate.cs index 669030d..efeb419 100644 --- a/src/ShellUI.Templates/Templates/SheetTemplate.cs +++ b/src/ShellUI.Templates/Templates/SheetTemplate.cs @@ -16,12 +16,11 @@ public class SheetTemplate }; public static string Content => @"@namespace YourProjectNamespace.Components.UI -@using YourProjectNamespace.Components.UI.Variants @using Microsoft.JSInterop @implements IAsyncDisposable @inject IJSRuntime JS -@if (Compositional) +@if (UseCompositional) { @ChildContent @@ -65,10 +64,14 @@ @inject IJSRuntime JS [Parameter] public SheetSide Side { get; set; } = SheetSide.Right; [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public string? Class { get; set; } - [Parameter] public bool Compositional { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + // Compositional mode also turns on when Title and Description are both empty. + [Parameter] public bool Compositional { get; set; } + + private bool UseCompositional => Compositional || (string.IsNullOrEmpty(Title) && string.IsNullOrEmpty(Description)); + private async Task Close() { Open = false; @@ -93,7 +96,7 @@ private async Task OnPanelKeyDownAsync(KeyboardEventArgs e) protected override async Task OnAfterRenderAsync(bool firstRender) { - var panelOpen = Open && !Compositional; + var panelOpen = Open && !UseCompositional; if (panelOpen && !_focused) { _focused = true; diff --git a/src/ShellUI.Templates/Templates/StepperContentTemplate.cs b/src/ShellUI.Templates/Templates/StepperContentTemplate.cs index 7b5e9d2..bc56482 100644 --- a/src/ShellUI.Templates/Templates/StepperContentTemplate.cs +++ b/src/ShellUI.Templates/Templates/StepperContentTemplate.cs @@ -16,7 +16,7 @@ public static class StepperContentTemplate public static string Content => @"@namespace YourProjectNamespace.Components.UI -@if (ParentStepper != null && ParentStepper.CurrentStep == Index) +@if (ParentStepper != null && ParentStepper.ActiveStep == Index) {
@ChildContent diff --git a/src/ShellUI.Templates/Templates/StepperModelsTemplate.cs b/src/ShellUI.Templates/Templates/StepperModelsTemplate.cs new file mode 100644 index 0000000..8951498 --- /dev/null +++ b/src/ShellUI.Templates/Templates/StepperModelsTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class StepperModelsTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "stepper-models", + DisplayName = "Stepper Models", + Description = "Models for Stepper", + Category = ComponentCategory.Navigation, + FilePath = "Models/StepperModels.cs", + IsAvailable = false, + Dependencies = new List() + }; + + public static string Content => @"using Microsoft.AspNetCore.Components; + +namespace YourProjectNamespace.Components.Models; + +public class StepItem +{ + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public RenderFragment? Content { get; set; } + public bool IsCompleted { get; set; } + public bool IsOptional { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/StepperStepTemplate.cs b/src/ShellUI.Templates/Templates/StepperStepTemplate.cs index 7d2ab02..2229788 100644 --- a/src/ShellUI.Templates/Templates/StepperStepTemplate.cs +++ b/src/ShellUI.Templates/Templates/StepperStepTemplate.cs @@ -18,10 +18,11 @@ public static class StepperStepTemplate @{ var index = int.TryParse(Value, out var v) ? v : 0; - var isActive = ParentStepper?.CurrentStep == index; + var isActive = ParentStepper?.ActiveStep == index; var isCompleted = ParentStepper != null && index < ParentStepper.HighestStepReached; var isClickable = ParentStepper != null && index <= ParentStepper.HighestStepReached; + // Active wins over completed, so a revisited step shows as current. string stateClass; if (isActive) stateClass = ""border-primary bg-primary text-primary-foreground ring-2 ring-primary/20 ring-offset-2 ring-offset-background""; diff --git a/src/ShellUI.Templates/Templates/StepperTemplate.cs b/src/ShellUI.Templates/Templates/StepperTemplate.cs index 1bbe0ae..db5e680 100644 --- a/src/ShellUI.Templates/Templates/StepperTemplate.cs +++ b/src/ShellUI.Templates/Templates/StepperTemplate.cs @@ -12,52 +12,122 @@ public static class StepperTemplate Category = ComponentCategory.Navigation, FilePath = "Stepper.razor", Tags = new List { "stepper", "steps", "progress", "wizard", "multi-step" }, - Dependencies = new List { "stepper-list", "stepper-step", "stepper-content" } + Dependencies = new List { "stepper-models", "stepper-list", "stepper-step", "stepper-content" } }; public const string Content = """ @namespace YourProjectNamespace.Components.UI +@using YourProjectNamespace.Components.Models
- @ChildContent + @if (StepItems.Count > 0) + { + @* Data-driven mode (StepItems) *@ +
+ @for (int i = 0; i < StepItems.Count; i++) + { + var step = StepItems[i]; + var isActive = i == _activeStep; + var isCompleted = i < _highestStepReached; + var isClickable = i <= _highestStepReached; + var capturedIndex = i; + + string stateClass; + if (isActive) + stateClass = "border-primary bg-primary text-primary-foreground ring-2 ring-primary/20 ring-offset-2 ring-offset-background"; + else if (isCompleted) + stateClass = "border-primary bg-primary/10 text-primary"; + else + stateClass = "border-muted bg-background text-muted-foreground"; + +
+ +
+

@step.Title

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

@step.Description

+ } +
+ @if (i < StepItems.Count - 1) + { +
+ } +
+ } +
+ @if (_activeStep < StepItems.Count) + { +
+ @StepItems[_activeStep].Content +
+ } + } + else + { + @* Compositional mode (StepperList + StepperContent) *@ + @ChildContent + } + @if (ShowNavigation) { @if (NavigationContent != null) { -
@NavigationContent
+
+ @NavigationContent +
} else { -
- - @if (IsLastStep && ShowConfirmOnLast) - { - - } - else - { - - } -
+ + @if (IsLastStep && ShowConfirmOnLast) + { + + } + else + { + + } +
} }
@code { - [Parameter] public int TotalSteps { get; set; } = 3; + [Parameter] public List StepItems { get; set; } = new(); + [Parameter] public int TotalSteps { get; set; } [Parameter] public int CurrentStep { get; set; } = 0; [Parameter] public EventCallback CurrentStepChanged { get; set; } [Parameter] public EventCallback OnConfirm { get; set; } @@ -71,13 +141,19 @@ @namespace YourProjectNamespace.Components.UI [Parameter] public string Class { get; set; } = string.Empty; private int _highestStepReached = 0; - private bool IsLastStep => CurrentStep >= TotalSteps - 1; + private int _activeStep = 0; + private bool _justNavigated; public int HighestStepReached => _highestStepReached; - public int StepCount => TotalSteps; + public int ActiveStep => _activeStep; + public int EffectiveStepCount => StepItems.Count > 0 ? StepItems.Count : (TotalSteps > 0 ? TotalSteps : 0); + public int StepCount => EffectiveStepCount; + private bool IsLastStep => _activeStep >= EffectiveStepCount - 1; protected override void OnParametersSet() { + if (!_justNavigated) + _activeStep = CurrentStep; if (CurrentStep > _highestStepReached) _highestStepReached = CurrentStep; } @@ -85,34 +161,40 @@ protected override void OnParametersSet() public async Task GoToStepAsync(int step) { if (step > _highestStepReached || step < 0) return; - CurrentStep = step; + _justNavigated = true; + _activeStep = step; await CurrentStepChanged.InvokeAsync(step); + _justNavigated = false; + StateHasChanged(); } private async Task PreviousStep() { - if (CurrentStep > 0) - await GoToStepAsync(CurrentStep - 1); + if (_activeStep > 0) + await GoToStepAsync(_activeStep - 1); } private async Task NextStep() { - if (CurrentStep < TotalSteps - 1) + if (_activeStep < EffectiveStepCount - 1) { - var nextStep = CurrentStep + 1; + var nextStep = _activeStep + 1; if (nextStep > _highestStepReached) _highestStepReached = nextStep; - CurrentStep = nextStep; + _justNavigated = true; + _activeStep = nextStep; await CurrentStepChanged.InvokeAsync(nextStep); + _justNavigated = false; + StateHasChanged(); } } private async Task HandleConfirm() { - if (TotalSteps > 0 && CurrentStep >= TotalSteps - 1) + if (EffectiveStepCount > 0 && _activeStep >= EffectiveStepCount - 1) { - if (_highestStepReached < TotalSteps - 1) - _highestStepReached = TotalSteps - 1; + if (_highestStepReached < EffectiveStepCount - 1) + _highestStepReached = EffectiveStepCount - 1; await OnConfirm.InvokeAsync(); } } diff --git a/src/ShellUI.Templates/Templates/TextareaTemplate.cs b/src/ShellUI.Templates/Templates/TextareaTemplate.cs index 8b9bc35..3c86448 100644 --- a/src/ShellUI.Templates/Templates/TextareaTemplate.cs +++ b/src/ShellUI.Templates/Templates/TextareaTemplate.cs @@ -12,39 +12,53 @@ public static class TextareaTemplate Category = ComponentCategory.Form, FilePath = "Textarea.razor", - Dependencies = new List() + Dependencies = new List { "textarea-variants" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using YourProjectNamespace.Components.UI.Variants - + @onfocus=""HandleFocus"" + @onblur=""HandleBlur""> @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] public InputVariant Variant { get; set; } = InputVariant.Default; + [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 string? Class { 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 string ComputedClass => TextareaVariants.Get(Variant, Class); + private async Task HandleInput(ChangeEventArgs e) { - Value = e.Value?.ToString(); - await ValueChanged.InvokeAsync(Value); + var newValue = e.Value?.ToString() ?? """"; + Value = newValue; + await ValueChanged.InvokeAsync(newValue); + } + + private async Task HandleFocus(FocusEventArgs e) + { + await OnFocus.InvokeAsync(e); + } + + private async Task HandleBlur(FocusEventArgs e) + { + await OnBlur.InvokeAsync(e); } } "; diff --git a/src/ShellUI.Templates/Templates/TextareaVariantsTemplate.cs b/src/ShellUI.Templates/Templates/TextareaVariantsTemplate.cs new file mode 100644 index 0000000..d3d688b --- /dev/null +++ b/src/ShellUI.Templates/Templates/TextareaVariantsTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class TextareaVariantsTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "textarea-variants", + DisplayName = "Textarea Variants", + Description = "Textarea utility classes (uses InputVariant)", + Category = ComponentCategory.Utility, + FilePath = "Variants/TextareaVariants.cs", + IsAvailable = false, + Dependencies = new List { "input-variants" } + }; + + public static string Content => @"namespace YourProjectNamespace.Components.UI.Variants; + +public static class TextareaVariants +{ + public static string Get(InputVariant variant = InputVariant.Default, string? className = null) + { + var baseClasses = ""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""; + + var variantClasses = variant switch + { + InputVariant.Error => ""border-destructive focus-visible:ring-destructive"", + _ => """" + }; + + return Shell.Cn(baseClasses, variantClasses, className); + } +} +"; +}