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
39 changes: 39 additions & 0 deletions ShellUI.Tests/TemplateSyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ public class TemplateSyncTests
[InlineData("command-option", "CommandOption.razor")]
[InlineData("command-empty", "CommandEmpty.razor")]
[InlineData("command-separator", "CommandSeparator.razor")]
[InlineData("label", "Label.razor")]
[InlineData("file-upload", "FileUpload.razor")]
[InlineData("carousel-item", "CarouselItem.razor")]
[InlineData("menubar", "Menubar.razor")]
[InlineData("sheet-content", "SheetContent.razor")]
[InlineData("drawer-content", "DrawerContent.razor")]
[InlineData("sidebar-menu-button", "SidebarMenuButton.razor")]
public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string razorFileName)
{
if (AllowedDrift.ContainsKey(templateName)) return;
Expand All @@ -152,6 +159,29 @@ public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string raz
DiffSummary(normalizedLive, normalizedTemplate));
}

[Theory]
[InlineData("label", "Label.razor")]
[InlineData("file-upload", "FileUpload.razor")]
[InlineData("carousel-item", "CarouselItem.razor")]
[InlineData("menubar", "Menubar.razor")]
[InlineData("sheet-content", "SheetContent.razor")]
[InlineData("drawer-content", "DrawerContent.razor")]
[InlineData("sidebar-menu-button", "SidebarMenuButton.razor")]
[InlineData("theme-toggle", "ThemeToggle.razor")]
public void TemplateMarkup_MatchesLiveLibrary(string templateName, string razorFileName)
{
var liveContent = File.ReadAllText(GetLiveRazorPath(razorFileName));
var templateContent = ComponentRegistry.GetComponentContent(templateName)
?? throw new InvalidOperationException($"Template '{templateName}' not found in registry");

var normalizedLive = Normalize(ExtractMarkup(liveContent));
var normalizedTemplate = Normalize(ExtractMarkup(templateContent));

Assert.True(normalizedLive == normalizedTemplate,
$"Markup drift between live {razorFileName} and template {templateName}.\n\n" +
DiffSummary(normalizedLive, normalizedTemplate));
}

[Fact]
public void ChartVariantsTemplate_MatchesLiveLibrary()
{
Expand Down Expand Up @@ -204,6 +234,15 @@ private static string DiffSummary(string live, string template)
return diffs.Length == 0 ? "(no per-line diff — file lengths differ)" : diffs.ToString();
}

// Everything before `@code`, without the @namespace and @using lines that differ by design.
private static string ExtractMarkup(string razor)
{
var match = Regex.Match(razor, @"@code\s*\{");
var head = match.Success ? razor.Substring(0, match.Index) : razor;
var lines = head.Split('\n').Where(l => !Regex.IsMatch(l.Trim(), @"^@(namespace|using)\b"));
return string.Join("\n", lines);
}

/// Body of the first `@code { }` block, or null if missing or unbalanced.
private static string? ExtractCodeBlock(string razor)
{
Expand Down
13 changes: 4 additions & 9 deletions docs/COMPONENT_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,10 @@ The current registry provides explicit sub-components for these parent component
- [x] **Popover** — `PopoverTrigger`, `PopoverContent`
- [x] **HoverCard** — `HoverCardTrigger`, `HoverCardContent`
- [x] **Drawer and Sheet** — explicit trigger/content and variant support

### Package only (CLI templates pending)

These parts exist in the `ShellUI.Components` NuGet package, but their CLI templates are empty placeholders, so CLI installs only get the parent component.

- [ ] **Select** — `SelectTrigger`, `SelectContent`, `SelectItem` (the native select works in both)
- [ ] **ContextMenu** — `ContextMenuTrigger`, `ContextMenuContent`, `ContextMenuOption`
- [ ] **NavigationMenu** — `NavList`, `NavItem`, `NavTrigger`, `NavContent`
- [ ] **Carousel** — `CarouselList`, `CarouselSlide`
- [x] **Select** — `SelectTrigger`, `SelectContent`, `SelectItem` (the native select still works)
- [x] **ContextMenu** — `ContextMenuTrigger`, `ContextMenuContent`, `ContextMenuOption`
- [x] **NavigationMenu** — `NavList`, `NavItem`, `NavTrigger`, `NavContent`
- [x] **Carousel** — `CarouselList`, `CarouselSlide`

The intended pattern is explicit child components, cascaded state and callbacks, value/label parameters, and Tailwind utility classes matching the shadcn-style API.

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 @@ -9,7 +9,7 @@
else if (Open)
{
<div class="fixed inset-0 z-50 bg-black/80 animate-in fade-in-0" @onclick="Close"></div>
<div class="@DrawerVariants.Get(Side, Class)" @attributes="AdditionalAttributes">
<div class="@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))
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Components/Components/DrawerContent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@if (Parent?.Open == true)
{
<div class="fixed inset-0 z-50 bg-black/80 animate-in fade-in-0" @onclick="Close"></div>
<div @ref="_content" tabindex="-1" role="dialog" aria-modal="true" @onkeydown="OnKeyDownAsync" class="outline-none @DrawerVariants.Get(Parent.Side, Class)" @attributes="AdditionalAttributes">
<div @ref="_content" tabindex="-1" role="dialog" aria-modal="true" @onkeydown="OnKeyDownAsync" class="outline-none @DrawerVariants.Get(Parent.Side, Class)" @onclick:stopPropagation="true" @attributes="AdditionalAttributes">
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted"></div>
<div class="flex flex-col gap-4 p-4">
@ChildContent
Expand Down
8 changes: 7 additions & 1 deletion src/ShellUI.Components/Components/FileUpload.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
@ondragover:preventDefault="true"
@ondrop:preventDefault="true">
<div class="text-center pointer-events-none">
<span class="material-symbols-outlined mx-auto text-5xl text-muted-foreground block">cloud_upload</span>
<svg class="mx-auto h-12 w-12 text-muted-foreground block"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M7 16a4 4 0 01-.88-7.9 5 5 0 019.9-1A5.5 5.5 0 0118.5 16H7z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 12v6m0-6l-2 2m2-2l2 2" />
</svg>
<div class="mt-4">
<p class="text-sm text-foreground font-medium">@Text</p>
@if (Multiple && AllowedFileCount > 1)
Expand Down
26 changes: 10 additions & 16 deletions src/ShellUI.Components/Components/Label.razor
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
@namespace ShellUI.Components

<label class="@CssClass" @attributes="AdditionalAttributes">
<label
for="@For"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
@attributes="AdditionalAttributes">
@ChildContent
</label>

@code {
[Parameter] public string For { get; set; } = "";
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }

private string CssClass => BuildCssClass();
[Parameter]
public string? For { get; set; }

private string BuildCssClass()
{
var classes = new List<string>
{
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
};
[Parameter]
public RenderFragment? ChildContent { get; set; }

return string.Join(" ", classes);
}
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }
}

28 changes: 16 additions & 12 deletions src/ShellUI.Components/Components/Sheet.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@
else if (Open)
{
<div class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm animate-in fade-in-0" @onclick="Close"></div>
<div class="@SheetVariants.Get(Side, Class)" @attributes="AdditionalAttributes">
<div class="@SheetVariants.Get(Side, Class)" @onclick:stopPropagation="true" @attributes="AdditionalAttributes">
<div class="flex flex-col space-y-2">
@if (!string.IsNullOrEmpty(Title))
{
<div class="flex items-center justify-between">
<div class="flex items-center justify-between">
@if (!string.IsNullOrEmpty(Title))
{
<h2 class="text-lg font-semibold text-foreground">@Title</h2>
<button type="button" @onclick="Close" class="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span class="sr-only">Close</span>
</button>
</div>
}
}
else
{
<span></span>
}
<button type="button" @onclick="Close" class="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span class="sr-only">Close</span>
</button>
</div>
@if (!string.IsNullOrEmpty(Description))
{
<p class="text-sm text-muted-foreground">@Description</p>
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Components/Components/SheetContent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@if (Parent?.Open == true)
{
<div class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm animate-in fade-in-0" @onclick="Close"></div>
<div @ref="_content" tabindex="-1" role="dialog" aria-modal="true" @onkeydown="OnKeyDownAsync" class="outline-none @SheetVariants.Get(Parent.Side, Class)" @attributes="AdditionalAttributes">
<div @ref="_content" tabindex="-1" role="dialog" aria-modal="true" @onkeydown="OnKeyDownAsync" class="outline-none @SheetVariants.Get(Parent.Side, Class)" @onclick:stopPropagation="true" @attributes="AdditionalAttributes">
<div class="flex flex-col gap-4 p-6">
@ChildContent
</div>
Expand Down
2 changes: 0 additions & 2 deletions src/ShellUI.Components/build/ShellUI.Components.targets
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@
<ShellUISafelistEntry Include="left-3.5" />
<ShellUISafelistEntry Include="lg:space-x-8" />
<ShellUISafelistEntry Include="list-none" />
<ShellUISafelistEntry Include="material-symbols-outlined" />
<ShellUISafelistEntry Include="max-h-48" />
<ShellUISafelistEntry Include="max-h-60" />
<ShellUISafelistEntry Include="max-h-[90vh]" />
Expand Down Expand Up @@ -352,7 +351,6 @@
<ShellUISafelistEntry Include="starting:scale-95" />
<ShellUISafelistEntry Include="tabular-nums" />
<ShellUISafelistEntry Include="text-2xl" />
<ShellUISafelistEntry Include="text-5xl" />
<ShellUISafelistEntry Include="text-[10px]" />
<ShellUISafelistEntry Include="text-[8px]" />
<ShellUISafelistEntry Include="text-accent-foreground" />
Expand Down
2 changes: 0 additions & 2 deletions src/ShellUI.Components/wwwroot/shellui-classes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ left-2
left-3.5
lg:space-x-8
list-none
material-symbols-outlined
max-h-48
max-h-60
max-h-[90vh]
Expand Down Expand Up @@ -349,7 +348,6 @@ starting:opacity-0
starting:scale-95
tabular-nums
text-2xl
text-5xl
text-[10px]
text-[8px]
text-accent-foreground
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Templates/Templates/CarouselItemTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class CarouselItemTemplate
};

public const string Content = """
<div class="@($"w-full flex-shrink-0 {Class}")" style="width: @(100.0 / TotalSlides)%">
<div class="@Shell.Cn("w-full flex-shrink-0", Class)" style="width: @(100.0 / Math.Max(1, TotalSlides))%">
<div class="w-full h-full flex items-center justify-center">
@ChildContent
</div>
Expand Down
14 changes: 4 additions & 10 deletions src/ShellUI.Templates/Templates/MenubarTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,15 @@ public class MenubarTemplate

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

<div class=""@(""flex h-10 items-center space-x-1 rounded-md border bg-background p-1 "" + ClassName + "" "" + Class)"" @attributes=""AdditionalAttributes"">
<div class=""@Shell.Cn(""flex h-10 items-center space-x-1 rounded-md border border-border bg-background p-1"", ClassName, Class)"" @attributes=""AdditionalAttributes"">
@ChildContent
</div>

@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }

[Parameter]
public string? Class { get; set; }

[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public string? Class { get; set; }
// Deprecated: use Class.
[Parameter]
public string ClassName { get; set; } = """";

[Parameter] public string ClassName { get; set; } = """";
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static class SidebarMenuButtonTemplate
};

var buttonClass = Shell.Cn(
""peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center"",
""peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:[&>span]:hidden [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:w-4 [&>i]:shrink-0 [&>i]:text-center"",
sizeClass,
Class
);
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Templates/Templates/ThemeToggleTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ @implements IAsyncDisposable

<button
@onclick=""ToggleTheme""
class=""inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 @(Variant == ""outline"" ? ""border border-input bg-background"" : """") hover:bg-accent hover:text-accent-foreground @(Size == ""sm"" ? ""h-8 w-8"" : Size == ""lg"" ? ""h-11 w-11"" : ""h-9 w-9"") @Class""
class=""@Shell.Cn(""inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground"", Variant == ""outline"" ? ""border border-input bg-background"" : """", Size == ""sm"" ? ""h-8 w-8"" : Size == ""lg"" ? ""h-11 w-11"" : ""h-9 w-9"", Class)""
title=""Toggle theme""
type=""button""
@attributes=""AdditionalAttributes"">
Expand Down
Loading