diff --git a/ShellUI.Tests/TemplateSyncTests.cs b/ShellUI.Tests/TemplateSyncTests.cs
index 4c1894e..a54226a 100644
--- a/ShellUI.Tests/TemplateSyncTests.cs
+++ b/ShellUI.Tests/TemplateSyncTests.cs
@@ -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;
@@ -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()
{
@@ -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)
{
diff --git a/docs/COMPONENT_ROADMAP.md b/docs/COMPONENT_ROADMAP.md
index 30af5bd..95b5b94 100644
--- a/docs/COMPONENT_ROADMAP.md
+++ b/docs/COMPONENT_ROADMAP.md
@@ -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.
diff --git a/src/ShellUI.Components/Components/Drawer.razor b/src/ShellUI.Components/Components/Drawer.razor
index 9dde347..76bd72d 100644
--- a/src/ShellUI.Components/Components/Drawer.razor
+++ b/src/ShellUI.Components/Components/Drawer.razor
@@ -9,7 +9,7 @@
else if (Open)
{
-
+
@if (!string.IsNullOrEmpty(Title))
diff --git a/src/ShellUI.Components/Components/DrawerContent.razor b/src/ShellUI.Components/Components/DrawerContent.razor
index a2510ae..d3ea80a 100644
--- a/src/ShellUI.Components/Components/DrawerContent.razor
+++ b/src/ShellUI.Components/Components/DrawerContent.razor
@@ -6,7 +6,7 @@
@if (Parent?.Open == true)
{
-
+
@ChildContent
diff --git a/src/ShellUI.Components/Components/FileUpload.razor b/src/ShellUI.Components/Components/FileUpload.razor
index 4a24a1a..af4421d 100644
--- a/src/ShellUI.Components/Components/FileUpload.razor
+++ b/src/ShellUI.Components/Components/FileUpload.razor
@@ -10,7 +10,13 @@
@ondragover:preventDefault="true"
@ondrop:preventDefault="true">
-
cloud_upload
+
@Text
@if (Multiple && AllowedFileCount > 1)
diff --git a/src/ShellUI.Components/Components/Label.razor b/src/ShellUI.Components/Components/Label.razor
index 5af8190..34fdd8e 100644
--- a/src/ShellUI.Components/Components/Label.razor
+++ b/src/ShellUI.Components/Components/Label.razor
@@ -1,25 +1,19 @@
@namespace ShellUI.Components
-