Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions ShellUI.Tests/PackageBehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>();

var stepper = ctx.Render<Stepper>(p => p
.Add(c => c.TotalSteps, 2)
.Add(c => c.CurrentStepChanged, EventCallback.Factory.Create<int>(this, v => changes.Add(v)))
.AddChildContent<StepperList>(l => l
.AddChildContent<StepperStep>(s => s.Add(c => c.Value, "0").Add(c => c.Title, "Account"))
.AddChildContent<StepperStep>(s => s.Add(c => c.Value, "1").Add(c => c.Title, "Profile")))
.AddChildContent<StepperContent>(c => c.Add(x => x.Value, "0").AddChildContent("First"))
.AddChildContent<StepperContent>(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()
{
Expand Down
3 changes: 3 additions & 0 deletions ShellUI.Tests/TemplateCompileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions ShellUI.Tests/TemplateSyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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)));
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Components/Components/Drawer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ else if (Open)
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? 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));
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Components/Components/Sheet.razor
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ else if (Open)
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? 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));
Expand Down
6 changes: 6 additions & 0 deletions src/ShellUI.Templates/ComponentRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -499,6 +504,7 @@ public static class ComponentRegistry
"chat-input" => ChatInputTemplate.Content,
"chat" => ChatTemplate.Content,
"command-models" => CommandModelsTemplate.Content,
"stepper-models" => StepperModelsTemplate.Content,
_ => null
};
}
Expand Down
41 changes: 22 additions & 19 deletions src/ShellUI.Templates/Templates/DrawerTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
<CascadingValue Value=""this"" IsFixed=""true"">
@ChildContent
Expand All @@ -31,22 +30,22 @@ @inject IJSRuntime JS
{
<div class=""fixed inset-0 z-50 bg-black/80 animate-in fade-in-0"" @onclick=""Close""></div>
<div @ref=""_panel"" tabindex=""-1"" role=""dialog"" aria-modal=""true"" @onkeydown=""OnPanelKeyDownAsync"" class=""outline-none @DrawerVariants.Get(Side, Class)"" @onclick:stopPropagation=""true"" @attributes=""AdditionalAttributes"">
<div class=""mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted""></div>
<div class=""grid gap-1.5 p-4 text-center sm:text-left"">
@if (!string.IsNullOrEmpty(Title))
{
<h2 class=""text-lg font-semibold leading-none tracking-tight"">@Title</h2>
}
@if (!string.IsNullOrEmpty(Description))
{
<p class=""text-sm text-muted-foreground"">@Description</p>
}
<div class=""mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted""></div>
<div class=""grid gap-1.5 p-4 text-center sm:text-left"">
@if (!string.IsNullOrEmpty(Title))
{
<h2 class=""text-lg font-semibold leading-none tracking-tight"">@Title</h2>
}
@if (!string.IsNullOrEmpty(Description))
{
<p class=""text-sm text-muted-foreground"">@Description</p>
}
</div>
<div class=""p-4"">@ChildContent</div>
<div class=""mt-auto flex flex-col gap-2 p-4"">
<button type=""button"" @onclick=""Close"" class=""inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-full"">Close</button>
</div>
</div>
<div class=""p-4"">@ChildContent</div>
<div class=""mt-auto flex flex-col gap-2 p-4"">
<button type=""button"" @onclick=""Close"" class=""inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-full"">Close</button>
</div>
</div>
}

@code {
Expand All @@ -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<string, object>? 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;
Expand All @@ -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;
Expand Down
23 changes: 3 additions & 20 deletions src/ShellUI.Templates/Templates/InputTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ public static class InputTemplate
Category = ComponentCategory.Form,

FilePath = "Input.razor",
Dependencies = new List<string>()
Dependencies = new List<string> { "input-variants" }
};

public static string Content => @"@namespace YourProjectNamespace.Components.UI
@using YourProjectNamespace.Components.UI.Variants

<input
<input
type=""@Type""
value=""@Value""
placeholder=""@Placeholder""
Expand Down Expand Up @@ -61,24 +62,6 @@ private async Task HandleBlur(FocusEventArgs e)
{
await OnBlur.InvokeAsync(e);
}

public enum InputVariant { Default, Error }

public static class InputVariants
{
public static string Get(InputVariant variant, string? className)
{
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);
}
}
}
";
}
Expand Down
41 changes: 41 additions & 0 deletions src/ShellUI.Templates/Templates/InputVariantsTemplate.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
";
}
11 changes: 7 additions & 4 deletions src/ShellUI.Templates/Templates/SheetTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
<CascadingValue Value=""this"" IsFixed=""true"">
@ChildContent
Expand Down Expand Up @@ -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<string, object>? 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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Templates/Templates/StepperContentTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
<div class=""mt-8"">
@ChildContent
Expand Down
31 changes: 31 additions & 0 deletions src/ShellUI.Templates/Templates/StepperModelsTemplate.cs
Original file line number Diff line number Diff line change
@@ -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<string>()
};

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; }
}
";
}
3 changes: 2 additions & 1 deletion src/ShellUI.Templates/Templates/StepperStepTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"";
Expand Down
Loading
Loading