From 6042d1514953e6117cb79a27beac03b90c1dd50b Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 1 Oct 2026 15:08:37 +0200 Subject: [PATCH 1/7] docs: update registry entry counts and component dependencies in documentation - Updated the `ComponentRegistry` entry count from 194 to 239 across multiple documentation files. - Adjusted hidden entry counts from 104 to 149 to reflect the current state of components and dependencies. - Enhanced details in `COMPONENT_DEPENDENCIES.md` and `COMPONENT_ROADMAP.md` to provide clearer insights into component relationships and availability. --- docs/ARCHITECTURE.md | 8 ++++---- docs/COMPARISON.md | 6 +++--- docs/COMPONENT_DEPENDENCIES.md | 34 ++++++++++++++++++++-------------- docs/COMPONENT_ROADMAP.md | 4 ++-- docs/CONTRIBUTING.md | 2 +- docs/PROJECT_STATUS.md | 4 ++-- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 30cf6b5..28609f4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -11,7 +11,7 @@ This document describes the ShellUI source tree. | Tailwind CSS | `4.3.2` | | Solution | `ShellUI.slnx` | | Solution projects | 6 | -| Registry | 194 entries: 90 direct targets and 104 hidden dependency entries | +| Registry | 239 entries: 90 direct targets and 149 hidden dependency entries | ShellUI is a CLI-first Blazor component library. The CLI copies source into a consumer project, while `ShellUI.Components` is an independent Razor class library (RCL) for consumers who prefer NuGet. @@ -89,13 +89,13 @@ The implemented command tree is: ## Registry Model and Counts -`ComponentRegistry` contains 194 metadata entries: +`ComponentRegistry` contains 239 metadata entries: | Registry classification | Count | Meaning | |---|---:|---| | Direct targets | 90 | Entries with `IsAvailable = true`; these are the normal targets shown by `shellui list`. | -| Hidden entries | 104 | Entries with `IsAvailable = false`; these are generally installed through a parent target. | -| **Total** | **194** | All registered templates, sub-components, variants, models, services, and assets. | +| Hidden entries | 149 | Entries with `IsAvailable = false`; these are generally installed through a parent target. | +| **Total** | **239** | All registered templates, sub-components, variants, models, services, and assets. | A dependency entry is still real source and is written to the consumer project when the dependency walk reaches it. The two counts describe registry visibility, not two different component libraries. diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index 399c7b9..235ca99 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -6,7 +6,7 @@ This is a qualitative comparison, not a benchmark report. Library features, pack - **Target framework:** .NET 10 - **Tailwind CSS:** `4.3.2` -- **CLI inventory:** 90 direct targets, 194 registry entries, and 104 hidden entries +- **CLI inventory:** 90 direct targets, 239 registry entries, and 149 hidden entries - **Distribution:** the CLI copies source into an application; the repository also contains the packable `ShellUI.Components` package - **Packaging boundary:** only `ShellUI.CLI` and `ShellUI.Components` are packable. `ShellUI.Core` and `ShellUI.Templates` are internal/non-packable projects. @@ -18,7 +18,7 @@ The direct-target count is the number users invoke with `shellui add`. It is not |---|---|---|---|---|---|---| | Distribution | CLI source-copy workflow; `ShellUI.Components` is packable | NuGet package | NuGet package | NuGet package | NuGet package | NuGet package | | CSS model | Tailwind CSS `4.3.2` | Material-oriented component styling | Project-specific component styling | Provider-based CSS options | Ant Design styling | Project-specific styling | -| Direct component count | 90 CLI targets; 194 registry entries; 104 hidden | Not audited here | Not audited here | Not audited here | Not audited here | Not audited here | +| Direct component count | 90 CLI targets; 239 registry entries; 149 hidden | Not audited here | Not audited here | Not audited here | Not audited here | Not audited here | | Source customization | Copied component source can be edited | Package/API customization | Package/API customization | Package/API customization | Package/API customization | Package/API customization | | CLI workflow | Yes | No CLI in this comparison | No CLI in this comparison | No CLI in this comparison | No CLI in this comparison | No CLI in this comparison | | Accessibility status | Semantic markup is used; no current WCAG conformance claim | Not audited here | Not audited here | Not audited here | Not audited here | Not audited here | @@ -39,7 +39,7 @@ ShellUI templates use Tailwind utility classes and CSS variables. ShellUI pins T ### Registry-driven dependencies -`ComponentRegistry` is authoritative for the 90 direct targets, 104 hidden support entries, source dependencies, and NuGet dependencies. A component's visible sub-components or support files do not imply that it is a standalone package. +`ComponentRegistry` is authoritative for the 90 direct targets, 149 hidden support entries, source dependencies, and NuGet dependencies. A component's visible sub-components or support files do not imply that it is a standalone package. ### Compositional APIs diff --git a/docs/COMPONENT_DEPENDENCIES.md b/docs/COMPONENT_DEPENDENCIES.md index d1a9ee7..7a6af2f 100644 --- a/docs/COMPONENT_DEPENDENCIES.md +++ b/docs/COMPONENT_DEPENDENCIES.md @@ -4,9 +4,9 @@ ## Registry snapshot -- **194** total registry entries +- **239** total registry entries - **90** direct CLI targets (`IsAvailable = true`) -- **104** hidden entries (`IsAvailable = false`) +- **149** hidden entries (`IsAvailable = false`) - `shellui list` displays the 90 direct targets; hidden sub-components, variants, models, services, and support assets are omitted from the public list. `Dependencies` contains registry-declared source/template dependencies. `NuGetDependencies` is separate and is used by the installer to add package references. A relationship visible in rendered markup is not automatically a registry dependency, so do not infer a dependency graph from component names or visual composition. @@ -17,50 +17,56 @@ The following table summarizes the non-empty `Dependencies` declared by the curr | Direct target | Declared source dependencies | Declared NuGet dependencies | |---|---|---| -| `accordion` | `accordion-type`, `accordion-item` | — | +| `accordion` | `accordion-type`, `accordion-item`, `accordion-trigger`, `accordion-content` | — | | `alert` | `alert-variants` | — | -| `alert-dialog` | `dialog`, `button` | — | +| `alert-dialog` | `dialog`, `button`, `alert-dialog-trigger`, `alert-dialog-content`, `alert-dialog-header`, `alert-dialog-title`, `alert-dialog-description`, `alert-dialog-footer`, `alert-dialog-action`, `alert-dialog-cancel` | — | | `area-chart` | `chart` | — | | `avatar` | `avatar-variants` | — | | `badge` | `badge-variants` | — | | `bar-chart` | `chart` | — | -| `breadcrumb` | `breadcrumb-item` | — | +| `breadcrumb` | `breadcrumb-item`, `breadcrumb-list`, `breadcrumb-link`, `breadcrumb-page`, `breadcrumb-separator`, `breadcrumb-ellipsis` | — | | `button` | `button-variants` | — | | `callout` | `callout-variants` | — | | `card` | `card-header`, `card-title`, `card-description`, `card-content`, `card-footer` | — | -| `carousel` | `carousel-item`, `carousel-content`, `carousel-previous`, `carousel-next`, `carousel-dots` | — | +| `carousel` | `carousel-item`, `carousel-content`, `carousel-previous`, `carousel-next`, `carousel-dots`, `carousel-list`, `carousel-slide` | — | | `chart` | `chart-variants`, `chart-styles` | `Blazor-ApexCharts` `6.0.2` | +| `chart-series` | `chart` | — | | `chat` | `chat-message`, `chat-input` | — | | `collapsible` | `collapsible-trigger`, `collapsible-content` | — | -| `command` | `command-models` | — | +| `command` | `command-models`, `command-input`, `command-list`, `command-group`, `command-option`, `command-empty`, `command-separator` | — | | `command-palette` | `command`, `shellui-js` | — | -| `context-menu` | `context-menu-models` | — | +| `context-menu` | `context-menu-models`, `context-menu-trigger`, `context-menu-content`, `context-menu-option` | — | | `copy-button` | `shellui-js` | — | | `dashboard-01` | `sidebar`, `breadcrumb`, `separator`, `theme-toggle`, `app-sidebar` | — | | `dashboard-02` | `sidebar`, `breadcrumb`, `separator`, `theme-toggle`, `app-sidebar` | — | | `data-table` | `data-table-models` | `System.Linq.Dynamic.Core` `1.7.1` | | `dialog` | `dialog-trigger`, `dialog-content`, `dialog-header`, `dialog-footer`, `dialog-title`, `dialog-description`, `dialog-close` | — | | `donut-chart` | `chart` | — | -| `drawer` | `drawer-variants`, `drawer-trigger`, `drawer-content` | — | +| `drawer` | `drawer-variants`, `drawer-trigger`, `drawer-content`, `drawer-header`, `drawer-title`, `drawer-description`, `drawer-footer`, `drawer-close` | — | +| `dropdown` | `dropdown-trigger`, `dropdown-content`, `dropdown-item` | — | | `empty-state` | `button` | — | | `file-upload` | `shellui-js` | — | -| `form` | `label`, `input`, `button` | — | +| `form` | `label`, `input`, `button`, `form-field`, `form-item`, `form-label`, `form-control`, `form-description`, `form-message` | — | +| `hover-card` | `hover-card-trigger`, `hover-card-content` | — | | `input-otp` | `shellui-js` | — | | `line-chart` | `chart` | — | -| `menubar` | `menubar-item` | — | +| `menubar` | `menubar-item`, `menubar-menu`, `menubar-trigger`, `menubar-content`, `menubar-separator` | — | | `multi-series-chart` | `chart`, `chart-series` | — | -| `navigation-menu` | `navigation-menu-item` | — | +| `navigation-menu` | `navigation-menu-item`, `nav-list`, `nav-item`, `nav-trigger`, `nav-content` | — | +| `pagination` | `pagination-content`, `pagination-item`, `pagination-link`, `pagination-previous`, `pagination-next`, `pagination-ellipsis` | — | | `pie-chart` | `chart` | — | +| `popover` | `popover-trigger`, `popover-content` | — | | `qr-code` | — | `QRCoder` `1.8.0` | | `radar-chart` | `chart` | — | | `radial-chart` | `chart` | — | | `radio-group` | `radio-group-item` | — | -| `sheet` | `sheet-variants`, `sheet-trigger`, `sheet-content` | — | +| `select` | `select-trigger`, `select-content`, `select-item` | — | +| `sheet` | `sheet-variants`, `sheet-trigger`, `sheet-content`, `sheet-header`, `sheet-title`, `sheet-description`, `sheet-footer`, `sheet-close` | — | | `sidebar` | `shell`, `sidebar-models`, `sidebar-provider`, `sidebar-header`, `sidebar-content`, `sidebar-footer`, `sidebar-group`, `sidebar-group-label`, `sidebar-group-content`, `sidebar-menu`, `sidebar-menu-item`, `sidebar-menu-button`, `sidebar-menu-sub`, `sidebar-menu-sub-item`, `sidebar-menu-sub-button`, `sidebar-menu-action`, `sidebar-menu-badge`, `sidebar-separator`, `sidebar-trigger`, `sidebar-inset`, `sidebar-rail` | — | | `sonner` | `sonner-variants`, `sonner-service` | — | | `stepper` | `stepper-list`, `stepper-step`, `stepper-content` | — | | `table` | `table-header`, `table-body`, `table-row`, `table-cell`, `table-head` | — | -| `tabs` | `tabs-list`, `tabs-trigger`, `tabs-content` | — | +| `tabs` | `tabs-list`, `tabs-trigger`, `tabs-content`, `tab-models` | — | | `theme-toggle` | `shellui-js` | — | | `timeline` | `timeline-item` | — | | `toggle` | `toggle-variants` | — | diff --git a/docs/COMPONENT_ROADMAP.md b/docs/COMPONENT_ROADMAP.md index b9b3c9b..5d3bd92 100644 --- a/docs/COMPONENT_ROADMAP.md +++ b/docs/COMPONENT_ROADMAP.md @@ -6,10 +6,10 @@ ## Current inventory -`ComponentRegistry` is the source of truth for component availability, names, and dependencies. The current registry contains **194 entries**: +`ComponentRegistry` is the source of truth for component availability, names, and dependencies. The current registry contains **239 entries**: - **90 direct CLI targets** shown by `shellui list` -- **104 hidden entries** for sub-components, variants, models, services, and support assets +- **149 hidden entries** for sub-components, variants, models, services, and support assets - **Packable projects:** `ShellUI.CLI` and `ShellUI.Components` Hidden entries are not counted as direct targets. They can still be installed recursively when a parent target declares them. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 4d09084..91de6d9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -93,7 +93,7 @@ For a component change: 1. Update the live component and its generated template together. 2. Update registry metadata, dependencies, `FilePath`, variants, and NuGet dependencies when the public contract changes. -3. Preserve the distinction between direct targets and hidden dependency entries. The current registry has 194 entries, including 90 direct targets and 104 hidden entries. +3. Preserve the distinction between direct targets and hidden dependency entries. The current registry has 239 entries, including 90 direct targets and 149 hidden entries. 4. Use the Tailwind `4.3.2` variable and utility conventions already used by the project. 5. Add or update tests that exercise the changed registry, template, or CLI behavior. 6. Review keyboard and assistive-technology behavior without claiming a blanket accessibility conformance level that the repository does not test. diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index b2e50b0..326da2c 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -42,10 +42,10 @@ theme update ### Registry and components -`ComponentRegistry` has 194 entries: +`ComponentRegistry` has 239 entries: - 90 direct targets with `IsAvailable = true`. -- 104 hidden entries with `IsAvailable = false`, generally installed as dependencies or assets. +- 149 hidden entries with `IsAvailable = false`, generally installed as dependencies or assets. The direct-target count is the number shown by the normal public list. The hidden count is not a second public library; it represents the sub-components, variants, models, services, and assets needed to make the direct targets work. From 6bc3ffd1ced540a645c51b52f7e10ffc03fb54c2 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 1 Oct 2026 15:08:50 +0200 Subject: [PATCH 2/7] test: add new tests for package namespace declarations in components and templates - Introduced a test to ensure all components in the ShellUI.Components namespace declare the correct package namespace in their .razor files. - Added a test to verify that templates do not reference the ShellUI.* namespaces, ensuring compatibility when copied into user projects. - Expanded the TemplateSyncTests with additional inline data for various UI components to enhance coverage. --- ShellUI.Tests/ClassParameterTests.cs | 11 ++++++ ShellUI.Tests/TemplateCompileTests.cs | 11 ++++++ ShellUI.Tests/TemplateSyncTests.cs | 50 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/ShellUI.Tests/ClassParameterTests.cs b/ShellUI.Tests/ClassParameterTests.cs index 784a9e3..cf633f3 100644 --- a/ShellUI.Tests/ClassParameterTests.cs +++ b/ShellUI.Tests/ClassParameterTests.cs @@ -50,6 +50,17 @@ public void EveryPackageComponentWithClassName_AlsoAcceptsAndRendersClass() string.Join("\n ", offenders)); } + // Without it the component lands in ShellUI.Components.Components, which `@using ShellUI.Components` doesn't reach. + [Fact] + public void EveryPackageComponent_DeclaresThePackageNamespace() + { + var missing = Directory.GetFiles(GetComponentsDirectory(), "*.razor") + .Where(path => !Regex.IsMatch(File.ReadAllText(path), @"^@namespace ShellUI\.Components\s*$", RegexOptions.Multiline)) + .Select(Path.GetFileName) + .ToList(); + Assert.True(missing.Count == 0, "Package components without `@namespace ShellUI.Components`:\n " + string.Join("\n ", missing)); + } + private static string GetComponentsDirectory([CallerFilePath] string thisFile = "") { var testDir = Path.GetDirectoryName(thisFile) ?? throw new InvalidOperationException("CallerFilePath is empty"); diff --git a/ShellUI.Tests/TemplateCompileTests.cs b/ShellUI.Tests/TemplateCompileTests.cs index 3295137..6433873 100644 --- a/ShellUI.Tests/TemplateCompileTests.cs +++ b/ShellUI.Tests/TemplateCompileTests.cs @@ -189,6 +189,17 @@ public void EveryDirectTarget_ImportsOnlyNamespacesItInstalls() Assert.True(failures.Count == 0, "Targets whose imports are not installed with them:\n " + string.Join("\n ", failures)); } + // Templates are copied into the user's project, where the NuGet package's namespace does not exist. + [Fact] + public void NoTemplate_ReferencesThePackageNamespace() + { + var packageNamespace = new Regex(@"\bShellUI\.Components\b|^\s*@?using\s+ShellUI\.", RegexOptions.Multiline); + var offenders = ComponentRegistry.Components.Keys + .Where(name => packageNamespace.IsMatch(ComponentRegistry.GetComponentContent(name) ?? "")) + .ToList(); + Assert.True(offenders.Count == 0, "Templates referencing ShellUI.* namespaces:\n " + string.Join("\n ", offenders)); + } + private static readonly (string Namespace, string Package)[] ThirdPartyNamespaces = { ("ApexCharts", "Blazor-ApexCharts"), diff --git a/ShellUI.Tests/TemplateSyncTests.cs b/ShellUI.Tests/TemplateSyncTests.cs index 3c64fc1..b5d012d 100644 --- a/ShellUI.Tests/TemplateSyncTests.cs +++ b/ShellUI.Tests/TemplateSyncTests.cs @@ -84,6 +84,56 @@ public class TemplateSyncTests [InlineData("accordion-item", "AccordionItem.razor")] [InlineData("accordion-trigger", "AccordionTrigger.razor")] [InlineData("accordion-content", "AccordionContent.razor")] + [InlineData("breadcrumb", "Breadcrumb.razor")] + [InlineData("breadcrumb-item", "BreadcrumbItem.razor")] + [InlineData("breadcrumb-list", "BreadcrumbList.razor")] + [InlineData("breadcrumb-link", "BreadcrumbLink.razor")] + [InlineData("breadcrumb-page", "BreadcrumbPage.razor")] + [InlineData("breadcrumb-separator", "BreadcrumbSeparator.razor")] + [InlineData("breadcrumb-ellipsis", "BreadcrumbEllipsis.razor")] + [InlineData("pagination-content", "PaginationContent.razor")] + [InlineData("pagination-item", "PaginationItem.razor")] + [InlineData("pagination-link", "PaginationLink.razor")] + [InlineData("pagination-previous", "PaginationPrevious.razor")] + [InlineData("pagination-next", "PaginationNext.razor")] + [InlineData("pagination-ellipsis", "PaginationEllipsis.razor")] + [InlineData("sheet-header", "SheetHeader.razor")] + [InlineData("sheet-title", "SheetTitle.razor")] + [InlineData("sheet-description", "SheetDescription.razor")] + [InlineData("sheet-footer", "SheetFooter.razor")] + [InlineData("sheet-close", "SheetClose.razor")] + [InlineData("drawer-header", "DrawerHeader.razor")] + [InlineData("drawer-title", "DrawerTitle.razor")] + [InlineData("drawer-description", "DrawerDescription.razor")] + [InlineData("drawer-footer", "DrawerFooter.razor")] + [InlineData("drawer-close", "DrawerClose.razor")] + [InlineData("alert-dialog", "AlertDialog.razor")] + [InlineData("alert-dialog-trigger", "AlertDialogTrigger.razor")] + [InlineData("alert-dialog-content", "AlertDialogContent.razor")] + [InlineData("alert-dialog-header", "AlertDialogHeader.razor")] + [InlineData("alert-dialog-title", "AlertDialogTitle.razor")] + [InlineData("alert-dialog-description", "AlertDialogDescription.razor")] + [InlineData("alert-dialog-footer", "AlertDialogFooter.razor")] + [InlineData("alert-dialog-action", "AlertDialogAction.razor")] + [InlineData("alert-dialog-cancel", "AlertDialogCancel.razor")] + [InlineData("form", "Form.razor")] + [InlineData("form-field", "FormField.razor")] + [InlineData("form-item", "FormItem.razor")] + [InlineData("form-label", "FormLabel.razor")] + [InlineData("form-control", "FormControl.razor")] + [InlineData("form-description", "FormDescription.razor")] + [InlineData("form-message", "FormMessage.razor")] + [InlineData("menubar-item", "MenubarItem.razor")] + [InlineData("menubar-menu", "MenubarMenu.razor")] + [InlineData("menubar-trigger", "MenubarTrigger.razor")] + [InlineData("menubar-content", "MenubarContent.razor")] + [InlineData("menubar-separator", "MenubarSeparator.razor")] + [InlineData("command-input", "CommandInput.razor")] + [InlineData("command-list", "CommandList.razor")] + [InlineData("command-group", "CommandGroup.razor")] + [InlineData("command-option", "CommandOption.razor")] + [InlineData("command-empty", "CommandEmpty.razor")] + [InlineData("command-separator", "CommandSeparator.razor")] public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string razorFileName) { if (AllowedDrift.ContainsKey(templateName)) return; From 7c368c62bd99c25c6c0a401055cb2ca3c208aa14 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 1 Oct 2026 15:09:20 +0200 Subject: [PATCH 3/7] feat: implement AlertDialog component with enhanced structure and new subcomponents - Refactored the AlertDialog component to support a legacy mode and introduced a new structure for better content management. - Added subcomponents: AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, and AlertDialogTrigger for improved modularity. - Enhanced the dialog's functionality with event callbacks for confirm and cancel actions, and improved accessibility features. - Updated the Breadcrumb component to conditionally render its content based on the presence of a BreadcrumbList. - Introduced new components for the Command palette, including CommandInput, CommandList, CommandOption, and CommandGroup, enhancing the command interface. - Added various utility components such as FormControl, FormField, and FormMessage to streamline form handling and validation. - Overall improvements to the component library structure and documentation for better usability and maintainability. --- .../Components/AlertDialog.razor | 98 ++++++++++--------- .../Components/AlertDialogAction.razor | 27 +++++ .../Components/AlertDialogCancel.razor | 26 +++++ .../Components/AlertDialogContent.razor | 59 +++++++++++ .../Components/AlertDialogDescription.razor | 12 +++ .../Components/AlertDialogFooter.razor | 12 +++ .../Components/AlertDialogHeader.razor | 12 +++ .../Components/AlertDialogTitle.razor | 12 +++ .../Components/AlertDialogTrigger.razor | 22 +++++ .../Components/Breadcrumb.razor | 13 ++- .../Components/BreadcrumbEllipsis.razor | 16 +++ .../Components/BreadcrumbItem.razor | 4 +- .../Components/BreadcrumbLink.razor | 13 +++ .../Components/BreadcrumbList.razor | 14 +++ .../Components/BreadcrumbPage.razor | 12 +++ .../Components/BreadcrumbSeparator.razor | 21 ++++ .../Components/Calendar.razor | 1 + .../Components/CarouselContent.razor | 1 + .../Components/CarouselDots.razor | 1 + .../Components/CarouselNext.razor | 1 + .../Components/CarouselPrevious.razor | 1 + .../Components/Command.razor | 66 ++++++++++++- .../Components/CommandEmpty.razor | 29 ++++++ .../Components/CommandGroup.razor | 41 ++++++++ .../Components/CommandInput.razor | 58 +++++++++++ .../Components/CommandList.razor | 12 +++ .../Components/CommandOption.razor | 62 ++++++++++++ .../Components/CommandPalette.razor | 1 - .../Components/CommandSeparator.razor | 9 ++ .../Components/DataTable.razor | 1 + .../Components/Drawer.razor | 5 +- .../Components/DrawerClose.razor | 17 ++++ .../Components/DrawerDescription.razor | 12 +++ .../Components/DrawerFooter.razor | 12 +++ .../Components/DrawerHeader.razor | 12 +++ .../Components/DrawerTitle.razor | 12 +++ .../Components/FileUpload.razor | 1 + src/ShellUI.Components/Components/Form.razor | 6 +- .../Components/FormControl.razor | 12 +++ .../Components/FormDescription.razor | 12 +++ .../Components/FormField.razor | 19 ++++ .../Components/FormItem.razor | 12 +++ .../Components/FormLabel.razor | 12 +++ .../Components/FormMessage.razor | 48 +++++++++ .../Components/MenubarContent.razor | 17 ++++ .../Components/MenubarItem.razor | 62 +++++++++--- .../Components/MenubarMenu.razor | 39 ++++++++ .../Components/MenubarSeparator.razor | 9 ++ .../Components/MenubarTrigger.razor | 19 ++++ .../Components/Pagination.razor | 75 +++++++------- .../Components/PaginationContent.razor | 12 +++ .../Components/PaginationEllipsis.razor | 16 +++ .../Components/PaginationItem.razor | 12 +++ .../Components/PaginationLink.razor | 40 ++++++++ .../Components/PaginationNext.razor | 16 +++ .../Components/PaginationPrevious.razor | 16 +++ src/ShellUI.Components/Components/Sheet.razor | 5 +- .../Components/SheetClose.razor | 17 ++++ .../Components/SheetDescription.razor | 12 +++ .../Components/SheetFooter.razor | 12 +++ .../Components/SheetHeader.razor | 12 +++ .../Components/SheetTitle.razor | 12 +++ 62 files changed, 1145 insertions(+), 107 deletions(-) create mode 100644 src/ShellUI.Components/Components/AlertDialogAction.razor create mode 100644 src/ShellUI.Components/Components/AlertDialogCancel.razor create mode 100644 src/ShellUI.Components/Components/AlertDialogContent.razor create mode 100644 src/ShellUI.Components/Components/AlertDialogDescription.razor create mode 100644 src/ShellUI.Components/Components/AlertDialogFooter.razor create mode 100644 src/ShellUI.Components/Components/AlertDialogHeader.razor create mode 100644 src/ShellUI.Components/Components/AlertDialogTitle.razor create mode 100644 src/ShellUI.Components/Components/AlertDialogTrigger.razor create mode 100644 src/ShellUI.Components/Components/BreadcrumbEllipsis.razor create mode 100644 src/ShellUI.Components/Components/BreadcrumbLink.razor create mode 100644 src/ShellUI.Components/Components/BreadcrumbList.razor create mode 100644 src/ShellUI.Components/Components/BreadcrumbPage.razor create mode 100644 src/ShellUI.Components/Components/BreadcrumbSeparator.razor create mode 100644 src/ShellUI.Components/Components/CommandEmpty.razor create mode 100644 src/ShellUI.Components/Components/CommandGroup.razor create mode 100644 src/ShellUI.Components/Components/CommandInput.razor create mode 100644 src/ShellUI.Components/Components/CommandList.razor create mode 100644 src/ShellUI.Components/Components/CommandOption.razor create mode 100644 src/ShellUI.Components/Components/CommandSeparator.razor create mode 100644 src/ShellUI.Components/Components/DrawerClose.razor create mode 100644 src/ShellUI.Components/Components/DrawerDescription.razor create mode 100644 src/ShellUI.Components/Components/DrawerFooter.razor create mode 100644 src/ShellUI.Components/Components/DrawerHeader.razor create mode 100644 src/ShellUI.Components/Components/DrawerTitle.razor create mode 100644 src/ShellUI.Components/Components/FormControl.razor create mode 100644 src/ShellUI.Components/Components/FormDescription.razor create mode 100644 src/ShellUI.Components/Components/FormField.razor create mode 100644 src/ShellUI.Components/Components/FormItem.razor create mode 100644 src/ShellUI.Components/Components/FormLabel.razor create mode 100644 src/ShellUI.Components/Components/FormMessage.razor create mode 100644 src/ShellUI.Components/Components/MenubarContent.razor create mode 100644 src/ShellUI.Components/Components/MenubarMenu.razor create mode 100644 src/ShellUI.Components/Components/MenubarSeparator.razor create mode 100644 src/ShellUI.Components/Components/MenubarTrigger.razor create mode 100644 src/ShellUI.Components/Components/PaginationContent.razor create mode 100644 src/ShellUI.Components/Components/PaginationEllipsis.razor create mode 100644 src/ShellUI.Components/Components/PaginationItem.razor create mode 100644 src/ShellUI.Components/Components/PaginationLink.razor create mode 100644 src/ShellUI.Components/Components/PaginationNext.razor create mode 100644 src/ShellUI.Components/Components/PaginationPrevious.razor create mode 100644 src/ShellUI.Components/Components/SheetClose.razor create mode 100644 src/ShellUI.Components/Components/SheetDescription.razor create mode 100644 src/ShellUI.Components/Components/SheetFooter.razor create mode 100644 src/ShellUI.Components/Components/SheetHeader.razor create mode 100644 src/ShellUI.Components/Components/SheetTitle.razor diff --git a/src/ShellUI.Components/Components/AlertDialog.razor b/src/ShellUI.Components/Components/AlertDialog.razor index 3f6fa57..50f467e 100644 --- a/src/ShellUI.Components/Components/AlertDialog.razor +++ b/src/ShellUI.Components/Components/AlertDialog.razor @@ -1,66 +1,68 @@ @namespace ShellUI.Components -@using ShellUI.Components - - - - @Title - @Description - - - @if (CancelText != null) - { - - } - - - - +@if (UseLegacy) +{ + + + + @Title + @Description + + + @if (CancelText != null) + { + + } + + + + +} +else +{ + + @ChildContent + +} @code { - [Parameter] - public bool IsOpen { get; set; } - - [Parameter] - public string Title { get; set; } = ""; - - [Parameter] - public string Description { get; set; } = ""; - - [Parameter] - public string ConfirmText { get; set; } = "Continue"; - - [Parameter] - public string? CancelText { get; set; } = "Cancel"; + [Parameter] public bool IsOpen { get; set; } + [Parameter] public EventCallback IsOpenChanged { get; set; } - [Parameter] - public ButtonVariant ConfirmVariant { get; set; } = ButtonVariant.Default; + [Parameter] public string? Title { get; set; } + [Parameter] public string? Description { get; set; } + [Parameter] public string ConfirmText { get; set; } = "Continue"; + [Parameter] public string? CancelText { get; set; } = "Cancel"; + [Parameter] public ButtonVariant ConfirmVariant { get; set; } = ButtonVariant.Default; + [Parameter] public EventCallback OnConfirm { get; set; } + [Parameter] public EventCallback OnCancel { get; set; } - [Parameter] - public EventCallback OnConfirm { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } - [Parameter] - public EventCallback OnCancel { get; set; } + private bool UseLegacy => !string.IsNullOrEmpty(Title) || !string.IsNullOrEmpty(Description); - [Parameter] - public EventCallback IsOpenChanged { get; set; } + public async Task SetOpen(bool value) + { + if (IsOpen != value) + { + IsOpen = value; + await IsOpenChanged.InvokeAsync(value); + StateHasChanged(); + } + } - private async Task HandleConfirm() + public async Task InvokeConfirm() { await OnConfirm.InvokeAsync(); - await CloseDialog(); + await SetOpen(false); } - private async Task HandleCancel() + public async Task InvokeCancel() { await OnCancel.InvokeAsync(); - await CloseDialog(); + await SetOpen(false); } - private async Task CloseDialog() - { - IsOpen = false; - await IsOpenChanged.InvokeAsync(IsOpen); - } + private async Task HandleConfirm() => await InvokeConfirm(); + private async Task HandleCancel() => await InvokeCancel(); } - diff --git a/src/ShellUI.Components/Components/AlertDialogAction.razor b/src/ShellUI.Components/Components/AlertDialogAction.razor new file mode 100644 index 0000000..d46241c --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogAction.razor @@ -0,0 +1,27 @@ +@namespace ShellUI.Components + + + +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public ButtonVariant Variant { get; set; } = ButtonVariant.Default; + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (OnClick.HasDelegate) + { + await OnClick.InvokeAsync(); + } + if (Parent != null) + { + await Parent.InvokeConfirm(); + } + } +} diff --git a/src/ShellUI.Components/Components/AlertDialogCancel.razor b/src/ShellUI.Components/Components/AlertDialogCancel.razor new file mode 100644 index 0000000..fba00bd --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogCancel.razor @@ -0,0 +1,26 @@ +@namespace ShellUI.Components + + + +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (OnClick.HasDelegate) + { + await OnClick.InvokeAsync(); + } + if (Parent != null) + { + await Parent.InvokeCancel(); + } + } +} diff --git a/src/ShellUI.Components/Components/AlertDialogContent.razor b/src/ShellUI.Components/Components/AlertDialogContent.razor new file mode 100644 index 0000000..e353ef8 --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogContent.razor @@ -0,0 +1,59 @@ +@namespace ShellUI.Components +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS + +@if (Parent?.IsOpen == true) +{ +
+
+ @ChildContent +
+} + +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isLocked; + private bool _focused; + private ElementReference _content; + + // Escape dismisses an alert dialog as Cancel; the backdrop does not. + private async Task OnKeyDownAsync(KeyboardEventArgs e) + { + if (e.Key == "Escape" && Parent != null) await Parent.InvokeCancel(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + var isOpen = Parent?.IsOpen == true; + if (isOpen && !_focused) + { + _focused = true; + try { await _content.FocusAsync(); } catch (InvalidOperationException) { } + } + else if (!isOpen) _focused = false; + + var shouldBeLocked = Parent?.IsOpen == true; + if (shouldBeLocked && !_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.lockBodyScroll"); _isLocked = true; } catch { } + } + else if (!shouldBeLocked && _isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); _isLocked = false; } catch { } + } + } + + public async ValueTask DisposeAsync() + { + if (_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); } catch { } + } + } +} diff --git a/src/ShellUI.Components/Components/AlertDialogDescription.razor b/src/ShellUI.Components/Components/AlertDialogDescription.razor new file mode 100644 index 0000000..9e3479c --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogDescription.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +

+ @ChildContent +

+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/AlertDialogFooter.razor b/src/ShellUI.Components/Components/AlertDialogFooter.razor new file mode 100644 index 0000000..3f22d57 --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogFooter.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/AlertDialogHeader.razor b/src/ShellUI.Components/Components/AlertDialogHeader.razor new file mode 100644 index 0000000..1d30313 --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogHeader.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/AlertDialogTitle.razor b/src/ShellUI.Components/Components/AlertDialogTitle.razor new file mode 100644 index 0000000..f1172c9 --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogTitle.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +

+ @ChildContent +

+ +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/AlertDialogTrigger.razor b/src/ShellUI.Components/Components/AlertDialogTrigger.razor new file mode 100644 index 0000000..0cb58dc --- /dev/null +++ b/src/ShellUI.Components/Components/AlertDialogTrigger.razor @@ -0,0 +1,22 @@ +@namespace ShellUI.Components + +
+ @ChildContent +
+ +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Open() + { + if (Parent != null) await Parent.SetOpen(true); + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if ((e.Key == "Enter" || e.Key == " ") && Parent != null) await Parent.SetOpen(true); + } +} diff --git a/src/ShellUI.Components/Components/Breadcrumb.razor b/src/ShellUI.Components/Components/Breadcrumb.razor index 8fa1fa7..5655881 100644 --- a/src/ShellUI.Components/Components/Breadcrumb.razor +++ b/src/ShellUI.Components/Components/Breadcrumb.razor @@ -1,14 +1,23 @@ @namespace ShellUI.Components @code { [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public string? Class { get; set; } + // Set when the children include a BreadcrumbList, which renders the
    itself. + [Parameter] public bool UseList { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } } diff --git a/src/ShellUI.Components/Components/BreadcrumbEllipsis.razor b/src/ShellUI.Components/Components/BreadcrumbEllipsis.razor new file mode 100644 index 0000000..dc32e7e --- /dev/null +++ b/src/ShellUI.Components/Components/BreadcrumbEllipsis.razor @@ -0,0 +1,16 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public string? Class { 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 index e36d1e8..f3ed6d4 100644 --- a/src/ShellUI.Components/Components/BreadcrumbItem.razor +++ b/src/ShellUI.Components/Components/BreadcrumbItem.razor @@ -14,7 +14,7 @@ } - @if (!IsLast) + @if (!IsLast && List == null) { @@ -23,6 +23,8 @@ @code { + // Inside a BreadcrumbList, separators come from BreadcrumbSeparator instead. + [CascadingParameter] private BreadcrumbList? List { get; set; } [Parameter] public string? Href { get; set; } [Parameter] public bool IsLast { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } diff --git a/src/ShellUI.Components/Components/BreadcrumbLink.razor b/src/ShellUI.Components/Components/BreadcrumbLink.razor new file mode 100644 index 0000000..2cef266 --- /dev/null +++ b/src/ShellUI.Components/Components/BreadcrumbLink.razor @@ -0,0 +1,13 @@ +@namespace ShellUI.Components + + + @ChildContent + + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/BreadcrumbList.razor b/src/ShellUI.Components/Components/BreadcrumbList.razor new file mode 100644 index 0000000..eb1036b --- /dev/null +++ b/src/ShellUI.Components/Components/BreadcrumbList.razor @@ -0,0 +1,14 @@ +@namespace ShellUI.Components + + +
      + @ChildContent +
    + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/BreadcrumbPage.razor b/src/ShellUI.Components/Components/BreadcrumbPage.razor new file mode 100644 index 0000000..7740f19 --- /dev/null +++ b/src/ShellUI.Components/Components/BreadcrumbPage.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + + + @ChildContent + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/BreadcrumbSeparator.razor b/src/ShellUI.Components/Components/BreadcrumbSeparator.razor new file mode 100644 index 0000000..28d7bc9 --- /dev/null +++ b/src/ShellUI.Components/Components/BreadcrumbSeparator.razor @@ -0,0 +1,21 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/Calendar.razor b/src/ShellUI.Components/Components/Calendar.razor index 0a318d2..fcb26a1 100644 --- a/src/ShellUI.Components/Components/Calendar.razor +++ b/src/ShellUI.Components/Components/Calendar.razor @@ -1,3 +1,4 @@ +@namespace ShellUI.Components @using System.Globalization
    diff --git a/src/ShellUI.Components/Components/CarouselContent.razor b/src/ShellUI.Components/Components/CarouselContent.razor index 47a0233..7fa95cc 100644 --- a/src/ShellUI.Components/Components/CarouselContent.razor +++ b/src/ShellUI.Components/Components/CarouselContent.razor @@ -1,3 +1,4 @@ +@namespace ShellUI.Components
    @ChildContent
    diff --git a/src/ShellUI.Components/Components/CarouselDots.razor b/src/ShellUI.Components/Components/CarouselDots.razor index 320d136..da8bac4 100644 --- a/src/ShellUI.Components/Components/CarouselDots.razor +++ b/src/ShellUI.Components/Components/CarouselDots.razor @@ -1,3 +1,4 @@ +@namespace ShellUI.Components
    @for (int i = 0; i < TotalSlides; i++) { diff --git a/src/ShellUI.Components/Components/CarouselNext.razor b/src/ShellUI.Components/Components/CarouselNext.razor index 5cb3ab2..3cb2882 100644 --- a/src/ShellUI.Components/Components/CarouselNext.razor +++ b/src/ShellUI.Components/Components/CarouselNext.razor @@ -1,3 +1,4 @@ +@namespace ShellUI.Components - +@if (!string.IsNullOrEmpty(Title)) +{ +
    + + + @if (IsOpen) + { +
    + @ChildContent +
    + } +
    + @if (IsOpen) { -
    - @ChildContent -
    +
    } -
    - -@if (IsOpen) +} +else { -
    + } @code { + [CascadingParameter] private MenubarMenu? Menu { get; set; } + // Set for the dropdown form; leave empty for a clickable item. [Parameter] public string Title { get; set; } = ""; [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public bool Disabled { get; set; } [Parameter] public string? Class { get; set; } // Deprecated: use Class. [Parameter] public string ClassName { get; set; } = ""; [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } - + private bool IsOpen { get; set; } - + + private string ItemClass => Shell.Cn( + Menu != null + ? "relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground" + : "inline-flex items-center rounded-sm px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + "disabled:pointer-events-none disabled:opacity-50", + ClassName, Class); + private void Toggle() => IsOpen = !IsOpen; private void Close() => IsOpen = false; + + private async Task Select(MouseEventArgs e) + { + await OnClick.InvokeAsync(e); + Menu?.Close(); + } } diff --git a/src/ShellUI.Components/Components/MenubarMenu.razor b/src/ShellUI.Components/Components/MenubarMenu.razor new file mode 100644 index 0000000..bf85e11 --- /dev/null +++ b/src/ShellUI.Components/Components/MenubarMenu.razor @@ -0,0 +1,39 @@ +@namespace ShellUI.Components + + +
    + @ChildContent +
    +
    + +@if (IsOpen) +{ +
    +} + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + public bool IsOpen { get; private set; } + + public void Toggle() + { + IsOpen = !IsOpen; + StateHasChanged(); + } + + public void Open() + { + IsOpen = true; + StateHasChanged(); + } + + public void Close() + { + IsOpen = false; + StateHasChanged(); + } +} diff --git a/src/ShellUI.Components/Components/MenubarSeparator.razor b/src/ShellUI.Components/Components/MenubarSeparator.razor new file mode 100644 index 0000000..627b375 --- /dev/null +++ b/src/ShellUI.Components/Components/MenubarSeparator.razor @@ -0,0 +1,9 @@ +@namespace ShellUI.Components + +
    + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/MenubarTrigger.razor b/src/ShellUI.Components/Components/MenubarTrigger.razor new file mode 100644 index 0000000..2d007eb --- /dev/null +++ b/src/ShellUI.Components/Components/MenubarTrigger.razor @@ -0,0 +1,19 @@ +@namespace ShellUI.Components + + + +@code { + [CascadingParameter] private MenubarMenu? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private void Toggle() => Parent?.Toggle(); +} diff --git a/src/ShellUI.Components/Components/Pagination.razor b/src/ShellUI.Components/Components/Pagination.razor index ed271a2..a254589 100644 --- a/src/ShellUI.Components/Components/Pagination.razor +++ b/src/ShellUI.Components/Components/Pagination.razor @@ -1,49 +1,58 @@ @namespace ShellUI.Components - @code { [Parameter] public int CurrentPage { get; set; } = 1; [Parameter] public EventCallback CurrentPageChanged { get; set; } [Parameter] public int TotalPages { get; set; } = 1; + // Set to build the pagination from PaginationContent, PaginationItem and PaginationLink. + [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public string? Class { get; set; } // Deprecated: use Class. [Parameter] public string ClassName { get; set; } = ""; diff --git a/src/ShellUI.Components/Components/PaginationContent.razor b/src/ShellUI.Components/Components/PaginationContent.razor new file mode 100644 index 0000000..480896d --- /dev/null +++ b/src/ShellUI.Components/Components/PaginationContent.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +
      + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/PaginationEllipsis.razor b/src/ShellUI.Components/Components/PaginationEllipsis.razor new file mode 100644 index 0000000..0142353 --- /dev/null +++ b/src/ShellUI.Components/Components/PaginationEllipsis.razor @@ -0,0 +1,16 @@ +@namespace ShellUI.Components + + + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/PaginationItem.razor b/src/ShellUI.Components/Components/PaginationItem.razor new file mode 100644 index 0000000..686c9d8 --- /dev/null +++ b/src/ShellUI.Components/Components/PaginationItem.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +
  1. + @ChildContent +
  2. + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/PaginationLink.razor b/src/ShellUI.Components/Components/PaginationLink.razor new file mode 100644 index 0000000..ec7f652 --- /dev/null +++ b/src/ShellUI.Components/Components/PaginationLink.razor @@ -0,0 +1,40 @@ +@namespace ShellUI.Components + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public bool IsActive { get; set; } + [Parameter] public string Size { get; set; } = "icon"; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string ComposedClass => Shell.Cn( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + Size == "icon" ? "h-9 w-9" : "h-9 px-4 py-2", + IsActive + ? "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground" + : "hover:bg-accent hover:text-accent-foreground", + Class); +} diff --git a/src/ShellUI.Components/Components/PaginationNext.razor b/src/ShellUI.Components/Components/PaginationNext.razor new file mode 100644 index 0000000..c2d46af --- /dev/null +++ b/src/ShellUI.Components/Components/PaginationNext.razor @@ -0,0 +1,16 @@ +@namespace ShellUI.Components + + + Next + + + + + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/PaginationPrevious.razor b/src/ShellUI.Components/Components/PaginationPrevious.razor new file mode 100644 index 0000000..0921761 --- /dev/null +++ b/src/ShellUI.Components/Components/PaginationPrevious.razor @@ -0,0 +1,16 @@ +@namespace ShellUI.Components + + + + + + Previous + + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { 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 index fba942f..6fe6fca 100644 --- a/src/ShellUI.Components/Components/Sheet.razor +++ b/src/ShellUI.Components/Components/Sheet.razor @@ -43,7 +43,10 @@ else if (Open) [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } - private bool UseCompositional => string.IsNullOrEmpty(Title) && string.IsNullOrEmpty(Description); + // CLI templates need Compositional="true"; the package also switches when Title and Description are empty. + [Parameter] public bool Compositional { get; set; } + + private bool UseCompositional => Compositional || (string.IsNullOrEmpty(Title) && string.IsNullOrEmpty(Description)); private async Task Close() { diff --git a/src/ShellUI.Components/Components/SheetClose.razor b/src/ShellUI.Components/Components/SheetClose.razor new file mode 100644 index 0000000..5654418 --- /dev/null +++ b/src/ShellUI.Components/Components/SheetClose.razor @@ -0,0 +1,17 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
    + +@code { + [CascadingParameter] private Sheet? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + if (Parent != null) await Parent.SetOpen(false); + } +} diff --git a/src/ShellUI.Components/Components/SheetDescription.razor b/src/ShellUI.Components/Components/SheetDescription.razor new file mode 100644 index 0000000..9e3479c --- /dev/null +++ b/src/ShellUI.Components/Components/SheetDescription.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +

    + @ChildContent +

    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SheetFooter.razor b/src/ShellUI.Components/Components/SheetFooter.razor new file mode 100644 index 0000000..3f22d57 --- /dev/null +++ b/src/ShellUI.Components/Components/SheetFooter.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SheetHeader.razor b/src/ShellUI.Components/Components/SheetHeader.razor new file mode 100644 index 0000000..1d30313 --- /dev/null +++ b/src/ShellUI.Components/Components/SheetHeader.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ShellUI.Components/Components/SheetTitle.razor b/src/ShellUI.Components/Components/SheetTitle.razor new file mode 100644 index 0000000..54adea0 --- /dev/null +++ b/src/ShellUI.Components/Components/SheetTitle.razor @@ -0,0 +1,12 @@ +@namespace ShellUI.Components + +

    + @ChildContent +

    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} From b6ae97c435426032f07c54a210a2fc20e7a1cb90 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 1 Oct 2026 15:09:39 +0200 Subject: [PATCH 4/7] feat: add new templates for AlertDialog and Command components - Introduced multiple new templates for the AlertDialog component, including AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, and AlertDialogTrigger, enhancing modularity and usability. - Added Command component templates: CommandEmpty, CommandGroup, CommandInput, CommandList, CommandOption, and CommandSeparator, providing a comprehensive command palette interface. - Each template includes metadata and content structure to facilitate integration and customization within the ShellUI framework. --- .../Templates/AlertDialogActionTemplate.cs | 46 ++++++++ .../Templates/AlertDialogCancelTemplate.cs | 45 ++++++++ .../Templates/AlertDialogContentTemplate.cs | 78 ++++++++++++++ .../AlertDialogDescriptionTemplate.cs | 31 ++++++ .../Templates/AlertDialogFooterTemplate.cs | 31 ++++++ .../Templates/AlertDialogHeaderTemplate.cs | 31 ++++++ .../Templates/AlertDialogTemplate.cs | 102 +++++++++--------- .../Templates/AlertDialogTitleTemplate.cs | 31 ++++++ .../Templates/AlertDialogTriggerTemplate.cs | 41 +++++++ .../Templates/BreadcrumbEllipsisTemplate.cs | 35 ++++++ .../Templates/BreadcrumbItemTemplate.cs | 20 ++-- .../Templates/BreadcrumbLinkTemplate.cs | 32 ++++++ .../Templates/BreadcrumbListTemplate.cs | 33 ++++++ .../Templates/BreadcrumbPageTemplate.cs | 31 ++++++ .../Templates/BreadcrumbSeparatorTemplate.cs | 40 +++++++ .../Templates/BreadcrumbTemplate.cs | 23 ++-- .../Templates/CommandEmptyTemplate.cs | 48 +++++++++ .../Templates/CommandGroupTemplate.cs | 60 +++++++++++ .../Templates/CommandInputTemplate.cs | 77 +++++++++++++ .../Templates/CommandListTemplate.cs | 31 ++++++ .../Templates/CommandOptionTemplate.cs | 81 ++++++++++++++ .../Templates/CommandSeparatorTemplate.cs | 28 +++++ .../Templates/CommandTemplate.cs | 70 +++++++++++- .../Templates/DrawerCloseTemplate.cs | 36 +++++++ .../Templates/DrawerDescriptionTemplate.cs | 31 ++++++ .../Templates/DrawerFooterTemplate.cs | 31 ++++++ .../Templates/DrawerHeaderTemplate.cs | 31 ++++++ .../Templates/DrawerTemplate.cs | 2 +- .../Templates/DrawerTitleTemplate.cs | 31 ++++++ .../Templates/FormControlTemplate.cs | 31 ++++++ .../Templates/FormDescriptionTemplate.cs | 31 ++++++ .../Templates/FormFieldTemplate.cs | 38 +++++++ .../Templates/FormItemTemplate.cs | 31 ++++++ .../Templates/FormLabelTemplate.cs | 31 ++++++ .../Templates/FormMessageTemplate.cs | 67 ++++++++++++ .../Templates/FormTemplate.cs | 8 +- .../Templates/MenubarContentTemplate.cs | 36 +++++++ .../Templates/MenubarItemTemplate.cs | 80 ++++++++++---- .../Templates/MenubarMenuTemplate.cs | 58 ++++++++++ .../Templates/MenubarSeparatorTemplate.cs | 28 +++++ .../Templates/MenubarTemplate.cs | 2 +- .../Templates/MenubarTriggerTemplate.cs | 38 +++++++ .../Templates/PaginationContentTemplate.cs | 31 ++++++ .../Templates/PaginationEllipsisTemplate.cs | 35 ++++++ .../Templates/PaginationItemTemplate.cs | 31 ++++++ .../Templates/PaginationLinkTemplate.cs | 59 ++++++++++ .../Templates/PaginationNextTemplate.cs | 35 ++++++ .../Templates/PaginationPreviousTemplate.cs | 35 ++++++ .../Templates/PaginationTemplate.cs | 78 ++++++++------ .../Templates/SheetCloseTemplate.cs | 36 +++++++ .../Templates/SheetDescriptionTemplate.cs | 31 ++++++ .../Templates/SheetFooterTemplate.cs | 31 ++++++ .../Templates/SheetHeaderTemplate.cs | 31 ++++++ .../Templates/SheetTemplate.cs | 2 +- .../Templates/SheetTitleTemplate.cs | 31 ++++++ 55 files changed, 2017 insertions(+), 135 deletions(-) create mode 100644 src/ShellUI.Templates/Templates/AlertDialogActionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AlertDialogCancelTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AlertDialogContentTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AlertDialogDescriptionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AlertDialogFooterTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AlertDialogHeaderTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AlertDialogTitleTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/AlertDialogTriggerTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BreadcrumbEllipsisTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BreadcrumbLinkTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BreadcrumbListTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BreadcrumbPageTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/BreadcrumbSeparatorTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CommandEmptyTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CommandGroupTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CommandInputTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CommandListTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CommandOptionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/CommandSeparatorTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DrawerCloseTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DrawerDescriptionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DrawerFooterTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DrawerHeaderTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/DrawerTitleTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/FormControlTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/FormDescriptionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/FormFieldTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/FormItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/FormLabelTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/FormMessageTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/MenubarContentTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/MenubarMenuTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/MenubarSeparatorTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/MenubarTriggerTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PaginationContentTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PaginationEllipsisTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PaginationItemTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PaginationLinkTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PaginationNextTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/PaginationPreviousTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SheetCloseTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SheetDescriptionTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SheetFooterTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SheetHeaderTemplate.cs create mode 100644 src/ShellUI.Templates/Templates/SheetTitleTemplate.cs diff --git a/src/ShellUI.Templates/Templates/AlertDialogActionTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogActionTemplate.cs new file mode 100644 index 0000000..717580d --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogActionTemplate.cs @@ -0,0 +1,46 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogActionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-action", + DisplayName = "Alert Dialog Action", + Description = "Confirm action button for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogAction.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "action" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public ButtonVariant Variant { get; set; } = ButtonVariant.Default; + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (OnClick.HasDelegate) + { + await OnClick.InvokeAsync(); + } + if (Parent != null) + { + await Parent.InvokeConfirm(); + } + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogCancelTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogCancelTemplate.cs new file mode 100644 index 0000000..3164f35 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogCancelTemplate.cs @@ -0,0 +1,45 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogCancelTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-cancel", + DisplayName = "Alert Dialog Cancel", + Description = "Cancel button for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogCancel.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "cancel" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task HandleClick() + { + if (OnClick.HasDelegate) + { + await OnClick.InvokeAsync(); + } + if (Parent != null) + { + await Parent.InvokeCancel(); + } + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogContentTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogContentTemplate.cs new file mode 100644 index 0000000..1afa794 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogContentTemplate.cs @@ -0,0 +1,78 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogContentTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-content", + DisplayName = "Alert Dialog Content", + Description = "Content subcomponent for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogContent.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "content" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS + +@if (Parent?.IsOpen == true) +{ +
    +
    + @ChildContent +
    +} + +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private bool _isLocked; + private bool _focused; + private ElementReference _content; + + // Escape dismisses an alert dialog as Cancel; the backdrop does not. + private async Task OnKeyDownAsync(KeyboardEventArgs e) + { + if (e.Key == ""Escape"" && Parent != null) await Parent.InvokeCancel(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + var isOpen = Parent?.IsOpen == true; + if (isOpen && !_focused) + { + _focused = true; + try { await _content.FocusAsync(); } catch (InvalidOperationException) { } + } + else if (!isOpen) _focused = false; + + var shouldBeLocked = Parent?.IsOpen == true; + if (shouldBeLocked && !_isLocked) + { + try { await JS.InvokeVoidAsync(""ShellUI.lockBodyScroll""); _isLocked = true; } catch { } + } + else if (!shouldBeLocked && _isLocked) + { + try { await JS.InvokeVoidAsync(""ShellUI.unlockBodyScroll""); _isLocked = false; } catch { } + } + } + + public async ValueTask DisposeAsync() + { + if (_isLocked) + { + try { await JS.InvokeVoidAsync(""ShellUI.unlockBodyScroll""); } catch { } + } + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogDescriptionTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogDescriptionTemplate.cs new file mode 100644 index 0000000..77a5535 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogDescriptionTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogDescriptionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-description", + DisplayName = "Alert Dialog Description", + Description = "Description subcomponent for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogDescription.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "description" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +

    + @ChildContent +

    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogFooterTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogFooterTemplate.cs new file mode 100644 index 0000000..f57e802 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogFooterTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogFooterTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-footer", + DisplayName = "Alert Dialog Footer", + Description = "Footer subcomponent for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogFooter.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "footer" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogHeaderTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogHeaderTemplate.cs new file mode 100644 index 0000000..81caa16 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogHeaderTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogHeaderTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-header", + DisplayName = "Alert Dialog Header", + Description = "Header subcomponent for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogHeader.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "header" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogTemplate.cs index e481e46..bd74e45 100644 --- a/src/ShellUI.Templates/Templates/AlertDialogTemplate.cs +++ b/src/ShellUI.Templates/Templates/AlertDialogTemplate.cs @@ -11,74 +11,78 @@ public static class AlertDialogTemplate Description = "Modal dialog for confirmations and alerts", Category = ComponentCategory.Overlay, FilePath = "AlertDialog.razor", - Dependencies = new List { "dialog", "button" } + Dependencies = new List { "dialog", "button", "alert-dialog-trigger", "alert-dialog-content", "alert-dialog-header", "alert-dialog-title", "alert-dialog-description", "alert-dialog-footer", "alert-dialog-action", "alert-dialog-cancel" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI -@using YourProjectNamespace.Components.UI @using YourProjectNamespace.Components.UI.Variants - - - - @Title - @Description - - - @if (CancelText != null) - { - - } - - - - +@if (UseLegacy) +{ + + + + @Title + @Description + + + @if (CancelText != null) + { + + } + + + + +} +else +{ + + @ChildContent + +} @code { - [Parameter] - public bool IsOpen { get; set; } - - [Parameter] - public string Title { get; set; } = """"; - - [Parameter] - public string Description { get; set; } = """"; - - [Parameter] - public string ConfirmText { get; set; } = ""Continue""; + [Parameter] public bool IsOpen { get; set; } + [Parameter] public EventCallback IsOpenChanged { get; set; } - [Parameter] - public string? CancelText { get; set; } = ""Cancel""; + [Parameter] public string? Title { get; set; } + [Parameter] public string? Description { get; set; } + [Parameter] public string ConfirmText { get; set; } = ""Continue""; + [Parameter] public string? CancelText { get; set; } = ""Cancel""; + [Parameter] public ButtonVariant ConfirmVariant { get; set; } = ButtonVariant.Default; + [Parameter] public EventCallback OnConfirm { get; set; } + [Parameter] public EventCallback OnCancel { get; set; } - [Parameter] - public ButtonVariant ConfirmVariant { get; set; } = ButtonVariant.Default; + [Parameter] public RenderFragment? ChildContent { get; set; } - [Parameter] - public EventCallback OnConfirm { get; set; } + private bool UseLegacy => !string.IsNullOrEmpty(Title) || !string.IsNullOrEmpty(Description); - [Parameter] - public EventCallback OnCancel { get; set; } - - [Parameter] - public EventCallback IsOpenChanged { get; set; } + public async Task SetOpen(bool value) + { + if (IsOpen != value) + { + IsOpen = value; + await IsOpenChanged.InvokeAsync(value); + StateHasChanged(); + } + } - private async Task HandleConfirm() + public async Task InvokeConfirm() { await OnConfirm.InvokeAsync(); - await CloseDialog(); + await SetOpen(false); } - private async Task HandleCancel() + public async Task InvokeCancel() { await OnCancel.InvokeAsync(); - await CloseDialog(); + await SetOpen(false); } - private async Task CloseDialog() - { - IsOpen = false; - await IsOpenChanged.InvokeAsync(IsOpen); - } -}"; + private async Task HandleConfirm() => await InvokeConfirm(); + private async Task HandleCancel() => await InvokeCancel(); +} +"; } diff --git a/src/ShellUI.Templates/Templates/AlertDialogTitleTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogTitleTemplate.cs new file mode 100644 index 0000000..32bdefb --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogTitleTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogTitleTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-title", + DisplayName = "Alert Dialog Title", + Description = "Title subcomponent for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogTitle.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "title" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +

    + @ChildContent +

    + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/AlertDialogTriggerTemplate.cs b/src/ShellUI.Templates/Templates/AlertDialogTriggerTemplate.cs new file mode 100644 index 0000000..1dd5491 --- /dev/null +++ b/src/ShellUI.Templates/Templates/AlertDialogTriggerTemplate.cs @@ -0,0 +1,41 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class AlertDialogTriggerTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "alert-dialog-trigger", + DisplayName = "Alert Dialog Trigger", + Description = "Trigger subcomponent for the compositional AlertDialog pattern", + Category = ComponentCategory.Overlay, + FilePath = "AlertDialogTrigger.razor", + IsAvailable = false, + Tags = new List { "overlay", "alert-dialog", "trigger" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
    + @ChildContent +
    + +@code { + [CascadingParameter] private AlertDialog? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Open() + { + if (Parent != null) await Parent.SetOpen(true); + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if ((e.Key == ""Enter"" || e.Key == "" "") && Parent != null) await Parent.SetOpen(true); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/BreadcrumbEllipsisTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbEllipsisTemplate.cs new file mode 100644 index 0000000..2754391 --- /dev/null +++ b/src/ShellUI.Templates/Templates/BreadcrumbEllipsisTemplate.cs @@ -0,0 +1,35 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class BreadcrumbEllipsisTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "breadcrumb-ellipsis", + DisplayName = "Breadcrumb Ellipsis", + Description = "Ellipsis placeholder for collapsed breadcrumb segments", + Category = ComponentCategory.Layout, + FilePath = "BreadcrumbEllipsis.razor", + IsAvailable = false, + Tags = new List { "navigation", "breadcrumb", "ellipsis" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + + + + + + More + + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs index e8f4568..4242d34 100644 --- a/src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs +++ b/src/ShellUI.Templates/Templates/BreadcrumbItemTemplate.cs @@ -31,7 +31,7 @@ public class BreadcrumbItemTemplate } - @if (!IsLast) + @if (!IsLast && List == null) { @@ -40,18 +40,12 @@ public class BreadcrumbItemTemplate @code { - [Parameter] - public string? Href { get; set; } - - [Parameter] - public bool IsLast { get; set; } - - [Parameter] - public RenderFragment? ChildContent { get; set; } - - [Parameter] - public string? Class { get; set; } - + // Inside a BreadcrumbList, separators come from BreadcrumbSeparator instead. + [CascadingParameter] private BreadcrumbList? List { get; set; } + [Parameter] public string? Href { get; set; } + [Parameter] public bool IsLast { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } } diff --git a/src/ShellUI.Templates/Templates/BreadcrumbLinkTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbLinkTemplate.cs new file mode 100644 index 0000000..3828b7f --- /dev/null +++ b/src/ShellUI.Templates/Templates/BreadcrumbLinkTemplate.cs @@ -0,0 +1,32 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class BreadcrumbLinkTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "breadcrumb-link", + DisplayName = "Breadcrumb Link", + Description = "Anchor subcomponent for the compositional Breadcrumb pattern", + Category = ComponentCategory.Layout, + FilePath = "BreadcrumbLink.razor", + IsAvailable = false, + Tags = new List { "navigation", "breadcrumb", "link" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @ChildContent + + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/BreadcrumbListTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbListTemplate.cs new file mode 100644 index 0000000..281011c --- /dev/null +++ b/src/ShellUI.Templates/Templates/BreadcrumbListTemplate.cs @@ -0,0 +1,33 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class BreadcrumbListTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "breadcrumb-list", + DisplayName = "Breadcrumb List", + Description = "Ordered-list wrapper for the compositional Breadcrumb pattern", + Category = ComponentCategory.Layout, + FilePath = "BreadcrumbList.razor", + IsAvailable = false, + Tags = new List { "navigation", "breadcrumb", "list" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + +
      + @ChildContent +
    + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/BreadcrumbPageTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbPageTemplate.cs new file mode 100644 index 0000000..256575d --- /dev/null +++ b/src/ShellUI.Templates/Templates/BreadcrumbPageTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class BreadcrumbPageTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "breadcrumb-page", + DisplayName = "Breadcrumb Page", + Description = "Current-page (unlinked) subcomponent for the compositional Breadcrumb pattern", + Category = ComponentCategory.Layout, + FilePath = "BreadcrumbPage.razor", + IsAvailable = false, + Tags = new List { "navigation", "breadcrumb", "page" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + @ChildContent + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/BreadcrumbSeparatorTemplate.cs b/src/ShellUI.Templates/Templates/BreadcrumbSeparatorTemplate.cs new file mode 100644 index 0000000..a12026e --- /dev/null +++ b/src/ShellUI.Templates/Templates/BreadcrumbSeparatorTemplate.cs @@ -0,0 +1,40 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class BreadcrumbSeparatorTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "breadcrumb-separator", + DisplayName = "Breadcrumb Separator", + Description = "Separator (default chevron) for the compositional Breadcrumb pattern", + Category = ComponentCategory.Layout, + FilePath = "BreadcrumbSeparator.razor", + IsAvailable = false, + Tags = new List { "navigation", "breadcrumb", "separator" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
  3. svg]:h-3.5 [&>svg]:w-3.5"", Class)"" @attributes=""AdditionalAttributes""> + @if (ChildContent != null) + { + @ChildContent + } + else + { + + + + } +
  4. + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { 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 index 03eb7af..4380ff8 100644 --- a/src/ShellUI.Templates/Templates/BreadcrumbTemplate.cs +++ b/src/ShellUI.Templates/Templates/BreadcrumbTemplate.cs @@ -13,24 +13,29 @@ public class BreadcrumbTemplate FilePath = "Breadcrumb.razor", Tags = new List { "navigation", "breadcrumb", "layout" }, - Dependencies = new List { "breadcrumb-item" } + Dependencies = new List { "breadcrumb-item", "breadcrumb-list", "breadcrumb-link", "breadcrumb-page", "breadcrumb-separator", "breadcrumb-ellipsis" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI @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; } + // Set when the children include a BreadcrumbList, which renders the
      itself. + [Parameter] public bool UseList { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } } diff --git a/src/ShellUI.Templates/Templates/CommandEmptyTemplate.cs b/src/ShellUI.Templates/Templates/CommandEmptyTemplate.cs new file mode 100644 index 0000000..a3b21f9 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CommandEmptyTemplate.cs @@ -0,0 +1,48 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class CommandEmptyTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "command-empty", + DisplayName = "Command Empty", + Description = "Shown when no Command option matches the search", + Category = ComponentCategory.Overlay, + FilePath = "CommandEmpty.razor", + IsAvailable = false, + Tags = new List { "command", "empty" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@implements IDisposable + +@if (Parent != null && Parent.VisibleCount == 0) +{ +
      + @ChildContent +
      +} + +@code { + [CascadingParameter] private Command? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + protected override void OnInitialized() + { + if (Parent != null) Parent.Changed += OnChanged; + } + + private void OnChanged() => InvokeAsync(StateHasChanged); + + public void Dispose() + { + if (Parent != null) Parent.Changed -= OnChanged; + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/CommandGroupTemplate.cs b/src/ShellUI.Templates/Templates/CommandGroupTemplate.cs new file mode 100644 index 0000000..6b488a7 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CommandGroupTemplate.cs @@ -0,0 +1,60 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class CommandGroupTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "command-group", + DisplayName = "Command Group", + Description = "Group of Command options with a heading", + Category = ComponentCategory.Overlay, + FilePath = "CommandGroup.razor", + IsAvailable = false, + Tags = new List { "command", "group" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@implements IDisposable + +@* Children stay rendered while hidden so their options keep their registration. *@ + + + + +@code { + [CascadingParameter] private Command? Parent { get; set; } + [Parameter] public string? Heading { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private readonly List _options = new(); + + private bool IsVisible => Parent == null || _options.Count == 0 || _options.Any(Parent.Matches); + + internal void Register(CommandOption option) => _options.Add(option); + internal void Unregister(CommandOption option) => _options.Remove(option); + + protected override void OnInitialized() + { + if (Parent != null) Parent.Changed += OnChanged; + } + + private void OnChanged() => InvokeAsync(StateHasChanged); + + public void Dispose() + { + if (Parent != null) Parent.Changed -= OnChanged; + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/CommandInputTemplate.cs b/src/ShellUI.Templates/Templates/CommandInputTemplate.cs new file mode 100644 index 0000000..e879d59 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CommandInputTemplate.cs @@ -0,0 +1,77 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class CommandInputTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "command-input", + DisplayName = "Command Input", + Description = "Search box for a compositional Command", + Category = ComponentCategory.Overlay, + FilePath = "CommandInput.razor", + IsAvailable = false, + Tags = new List { "command", "input" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + + + + +
      + +@code { + [CascadingParameter] private Command? Parent { get; set; } + [Parameter] public string Placeholder { get; set; } = ""Type a command or search...""; + [Parameter] public bool AutoFocus { get; set; } = true; + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private ElementReference _input; + private string _value = """"; + + private void OnInput(ChangeEventArgs e) + { + _value = e.Value?.ToString() ?? """"; + Parent?.SetSearch(_value); + } + + private async Task OnKeyDownAsync(KeyboardEventArgs e) + { + if (Parent == null) return; + switch (e.Key) + { + case ""ArrowDown"": Parent.MoveActive(1); break; + case ""ArrowUp"": Parent.MoveActive(-1); break; + case ""Home"": Parent.MoveActive(-1, toEnd: true); break; + case ""End"": Parent.MoveActive(1, toEnd: true); break; + case ""Enter"": await Parent.SelectActiveAsync(); break; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && AutoFocus) + { + try { await _input.FocusAsync(); } catch (InvalidOperationException) { } + } + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/CommandListTemplate.cs b/src/ShellUI.Templates/Templates/CommandListTemplate.cs new file mode 100644 index 0000000..fcf8d4e --- /dev/null +++ b/src/ShellUI.Templates/Templates/CommandListTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class CommandListTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "command-list", + DisplayName = "Command List", + Description = "Scrollable list of Command options", + Category = ComponentCategory.Overlay, + FilePath = "CommandList.razor", + IsAvailable = false, + Tags = new List { "command", "list" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/CommandOptionTemplate.cs b/src/ShellUI.Templates/Templates/CommandOptionTemplate.cs new file mode 100644 index 0000000..99ac359 --- /dev/null +++ b/src/ShellUI.Templates/Templates/CommandOptionTemplate.cs @@ -0,0 +1,81 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class CommandOptionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "command-option", + DisplayName = "Command Option", + Description = "Selectable option in a compositional Command", + Category = ComponentCategory.Overlay, + FilePath = "CommandOption.razor", + IsAvailable = false, + Tags = new List { "command", "option" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@implements IDisposable + +@* CommandOption rather than CommandItem: CommandItem is the model used by Command's Commands list. *@ +@if (IsVisible) +{ +
      + @ChildContent +
      +} + +@code { + [CascadingParameter] private Command? Parent { get; set; } + [CascadingParameter] private CommandGroup? Group { get; set; } + // The text matched against the search; the option is hidden while searching if it is empty. + [Parameter] public string? Value { get; set; } + [Parameter] public EventCallback OnSelect { get; set; } + [Parameter] public bool Disabled { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + internal string Text => Value ?? """"; + + private bool IsVisible => Parent == null || Parent.Matches(this); + private bool IsActive => Parent?.ActiveOption == this; + + protected override void OnInitialized() + { + Group?.Register(this); + if (Parent == null) return; + Parent.Register(this); + Parent.Changed += OnChanged; + } + + private void OnChanged() => InvokeAsync(StateHasChanged); + + private void Activate() + { + if (!Disabled) Parent?.SetActive(this); + } + + internal async Task SelectAsync() + { + if (!Disabled) await OnSelect.InvokeAsync(); + } + + public void Dispose() + { + Group?.Unregister(this); + if (Parent == null) return; + Parent.Changed -= OnChanged; + Parent.Unregister(this); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/CommandSeparatorTemplate.cs b/src/ShellUI.Templates/Templates/CommandSeparatorTemplate.cs new file mode 100644 index 0000000..0c7255f --- /dev/null +++ b/src/ShellUI.Templates/Templates/CommandSeparatorTemplate.cs @@ -0,0 +1,28 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class CommandSeparatorTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "command-separator", + DisplayName = "Command Separator", + Description = "Divider between Command groups", + Category = ComponentCategory.Overlay, + FilePath = "CommandSeparator.razor", + IsAvailable = false, + Tags = new List { "command", "separator" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/CommandTemplate.cs b/src/ShellUI.Templates/Templates/CommandTemplate.cs index c4b942b..6f3449f 100644 --- a/src/ShellUI.Templates/Templates/CommandTemplate.cs +++ b/src/ShellUI.Templates/Templates/CommandTemplate.cs @@ -11,12 +11,21 @@ public static class CommandTemplate Description = "Command palette component for quick actions", Category = ComponentCategory.Overlay, FilePath = "Command.razor", - Dependencies = new List { "command-models" } + Dependencies = new List { "command-models", "command-input", "command-list", "command-group", "command-option", "command-empty", "command-separator" } }; - public static string Content => @"@using YourProjectNamespace.Components.Models + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using YourProjectNamespace.Components.Models -@if (IsOpen) +@if (ChildContent != null) +{ + +
      + @ChildContent +
      +
      +} +else if (IsOpen) {
      Commands { get; set; } = new(); [Parameter] public EventCallback CommandSelected { get; set; } [Parameter] public string Placeholder { get; set; } = ""Type a command or search...""; + // Set to compose the command menu from CommandInput, CommandList, CommandGroup and CommandOption. + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } private readonly string _listId = $""command-list-{Guid.NewGuid():N}""; private readonly Dictionary _itemRefs = new(); @@ -125,6 +139,56 @@ public static class CommandTemplate private string ItemId(int index) => $""{_listId}-{index}""; + private readonly List _options = new(); + internal string Search { get; private set; } = """"; + internal CommandOption? ActiveOption { get; private set; } + internal event Action? Changed; + + internal bool Matches(CommandOption option) => + Search.Length == 0 || option.Text.Contains(Search, StringComparison.OrdinalIgnoreCase); + + internal int VisibleCount => _options.Count(Matches); + + internal void Register(CommandOption option) + { + _options.Add(option); + if (ActiveOption == null && Matches(option) && !option.Disabled) ActiveOption = option; + Changed?.Invoke(); + } + + internal void Unregister(CommandOption option) + { + _options.Remove(option); + if (ActiveOption == option) ActiveOption = _options.FirstOrDefault(o => Matches(o) && !o.Disabled); + Changed?.Invoke(); + } + + internal void SetSearch(string value) + { + Search = value.Trim(); + ActiveOption = _options.FirstOrDefault(o => Matches(o) && !o.Disabled); + Changed?.Invoke(); + } + + internal void SetActive(CommandOption option) + { + if (ActiveOption == option) return; + ActiveOption = option; + Changed?.Invoke(); + } + + internal void MoveActive(int delta, bool toEnd = false) + { + var visible = _options.Where(o => Matches(o) && !o.Disabled).ToList(); + if (visible.Count == 0) return; + var index = ActiveOption == null ? -1 : visible.IndexOf(ActiveOption); + index = toEnd ? (delta < 0 ? 0 : visible.Count - 1) : ((index + delta) % visible.Count + visible.Count) % visible.Count; + ActiveOption = visible[index]; + Changed?.Invoke(); + } + + internal Task SelectActiveAsync() => ActiveOption?.SelectAsync() ?? Task.CompletedTask; + protected override void OnParametersSet() { if (IsOpen && !_wasOpen) diff --git a/src/ShellUI.Templates/Templates/DrawerCloseTemplate.cs b/src/ShellUI.Templates/Templates/DrawerCloseTemplate.cs new file mode 100644 index 0000000..d91f0f7 --- /dev/null +++ b/src/ShellUI.Templates/Templates/DrawerCloseTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DrawerCloseTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "drawer-close", + DisplayName = "Drawer Close", + Description = "Close button subcomponent for the compositional Drawer pattern", + Category = ComponentCategory.Overlay, + FilePath = "DrawerClose.razor", + IsAvailable = false, + Tags = new List { "overlay", "drawer", "close" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
      + +@code { + [CascadingParameter] private Drawer? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + if (Parent != null) await Parent.SetOpen(false); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/DrawerDescriptionTemplate.cs b/src/ShellUI.Templates/Templates/DrawerDescriptionTemplate.cs new file mode 100644 index 0000000..6f8df26 --- /dev/null +++ b/src/ShellUI.Templates/Templates/DrawerDescriptionTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DrawerDescriptionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "drawer-description", + DisplayName = "Drawer Description", + Description = "Description subcomponent for the compositional Drawer pattern", + Category = ComponentCategory.Overlay, + FilePath = "DrawerDescription.razor", + IsAvailable = false, + Tags = new List { "overlay", "drawer", "description" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +

      + @ChildContent +

      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/DrawerFooterTemplate.cs b/src/ShellUI.Templates/Templates/DrawerFooterTemplate.cs new file mode 100644 index 0000000..fb0f337 --- /dev/null +++ b/src/ShellUI.Templates/Templates/DrawerFooterTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DrawerFooterTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "drawer-footer", + DisplayName = "Drawer Footer", + Description = "Footer subcomponent for the compositional Drawer pattern", + Category = ComponentCategory.Overlay, + FilePath = "DrawerFooter.razor", + IsAvailable = false, + Tags = new List { "overlay", "drawer", "footer" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/DrawerHeaderTemplate.cs b/src/ShellUI.Templates/Templates/DrawerHeaderTemplate.cs new file mode 100644 index 0000000..e0f0172 --- /dev/null +++ b/src/ShellUI.Templates/Templates/DrawerHeaderTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DrawerHeaderTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "drawer-header", + DisplayName = "Drawer Header", + Description = "Header subcomponent for the compositional Drawer pattern", + Category = ComponentCategory.Overlay, + FilePath = "DrawerHeader.razor", + IsAvailable = false, + Tags = new List { "overlay", "drawer", "header" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/DrawerTemplate.cs b/src/ShellUI.Templates/Templates/DrawerTemplate.cs index b5926bf..180d90e 100644 --- a/src/ShellUI.Templates/Templates/DrawerTemplate.cs +++ b/src/ShellUI.Templates/Templates/DrawerTemplate.cs @@ -11,7 +11,7 @@ public class DrawerTemplate Description = "Sliding drawer component with handle — supports compositional subcomponent pattern", Category = ComponentCategory.Overlay, FilePath = "Drawer.razor", - Dependencies = new List { "drawer-variants", "drawer-trigger", "drawer-content" }, + Dependencies = new List { "drawer-variants", "drawer-trigger", "drawer-content", "drawer-header", "drawer-title", "drawer-description", "drawer-footer", "drawer-close" }, Tags = new List { "overlay", "drawer", "modal", "slide" } }; diff --git a/src/ShellUI.Templates/Templates/DrawerTitleTemplate.cs b/src/ShellUI.Templates/Templates/DrawerTitleTemplate.cs new file mode 100644 index 0000000..cab386e --- /dev/null +++ b/src/ShellUI.Templates/Templates/DrawerTitleTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class DrawerTitleTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "drawer-title", + DisplayName = "Drawer Title", + Description = "Title subcomponent for the compositional Drawer pattern", + Category = ComponentCategory.Overlay, + FilePath = "DrawerTitle.razor", + IsAvailable = false, + Tags = new List { "overlay", "drawer", "title" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +

      + @ChildContent +

      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/FormControlTemplate.cs b/src/ShellUI.Templates/Templates/FormControlTemplate.cs new file mode 100644 index 0000000..8647949 --- /dev/null +++ b/src/ShellUI.Templates/Templates/FormControlTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class FormControlTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "form-control", + DisplayName = "Form Control", + Description = "Wrapper around the input control for the shadcn-style form pattern", + Category = ComponentCategory.Form, + FilePath = "FormControl.razor", + IsAvailable = false, + Tags = new List { "form", "control", "primitive" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/FormDescriptionTemplate.cs b/src/ShellUI.Templates/Templates/FormDescriptionTemplate.cs new file mode 100644 index 0000000..e849de8 --- /dev/null +++ b/src/ShellUI.Templates/Templates/FormDescriptionTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class FormDescriptionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "form-description", + DisplayName = "Form Description", + Description = "Helper text below a form control", + Category = ComponentCategory.Form, + FilePath = "FormDescription.razor", + IsAvailable = false, + Tags = new List { "form", "description", "primitive" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +

      + @ChildContent +

      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/FormFieldTemplate.cs b/src/ShellUI.Templates/Templates/FormFieldTemplate.cs new file mode 100644 index 0000000..54771fe --- /dev/null +++ b/src/ShellUI.Templates/Templates/FormFieldTemplate.cs @@ -0,0 +1,38 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class FormFieldTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "form-field", + DisplayName = "Form Field", + Description = "Generic field context primitive for the shadcn-style form pattern", + Category = ComponentCategory.Form, + FilePath = "FormField.razor", + IsAvailable = false, + Tags = new List { "form", "field", "primitive" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using System.Linq.Expressions +@using Microsoft.AspNetCore.Components.Forms + + + @ChildContent + + +@code { + [CascadingParameter] private EditContext? EditContext { get; set; } + [Parameter] public Expression>? For { get; set; } + [Parameter] public string? Name { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + + // The field FormMessage reports on: from For, or from Name on the EditForm model. + public FieldIdentifier Field => For != null + ? FieldIdentifier.Create(For) + : EditContext != null && !string.IsNullOrEmpty(Name) ? new FieldIdentifier(EditContext.Model, Name) : default; +} +"; +} diff --git a/src/ShellUI.Templates/Templates/FormItemTemplate.cs b/src/ShellUI.Templates/Templates/FormItemTemplate.cs new file mode 100644 index 0000000..28644b5 --- /dev/null +++ b/src/ShellUI.Templates/Templates/FormItemTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class FormItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "form-item", + DisplayName = "Form Item", + Description = "Vertical spacing wrapper for a single form row", + Category = ComponentCategory.Form, + FilePath = "FormItem.razor", + IsAvailable = false, + Tags = new List { "form", "item", "primitive" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + @ChildContent +
      + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/FormLabelTemplate.cs b/src/ShellUI.Templates/Templates/FormLabelTemplate.cs new file mode 100644 index 0000000..ae30597 --- /dev/null +++ b/src/ShellUI.Templates/Templates/FormLabelTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class FormLabelTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "form-label", + DisplayName = "Form Label", + Description = "Styled label for the shadcn-style form pattern", + Category = ComponentCategory.Form, + FilePath = "FormLabel.razor", + IsAvailable = false, + Tags = new List { "form", "label", "primitive" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/FormMessageTemplate.cs b/src/ShellUI.Templates/Templates/FormMessageTemplate.cs new file mode 100644 index 0000000..65518d0 --- /dev/null +++ b/src/ShellUI.Templates/Templates/FormMessageTemplate.cs @@ -0,0 +1,67 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class FormMessageTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "form-message", + DisplayName = "Form Message", + Description = "Validation-aware error message for a form field", + Category = ComponentCategory.Form, + FilePath = "FormMessage.razor", + IsAvailable = false, + Tags = new List { "form", "message", "validation" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI +@using Microsoft.AspNetCore.Components.Forms +@implements IDisposable + +@if (Messages.Length > 0) +{ +

      + @string.Join("", "", Messages) +

      +} + +@code { + [CascadingParameter] private EditContext? EditContext { get; set; } + [CascadingParameter(Name = ""ShellUIFormField"")] private FieldIdentifier Field { get; set; } + [Parameter] public string? Message { get; set; } + [Parameter] public string? FieldName { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private EditContext? _subscribed; + + private string[] Messages + { + get + { + if (!string.IsNullOrEmpty(Message)) return new[] { Message }; + if (EditContext == null) return Array.Empty(); + var target = string.IsNullOrEmpty(FieldName) ? Field : new FieldIdentifier(EditContext.Model, FieldName); + return target.Model == null ? Array.Empty() : EditContext.GetValidationMessages(target).ToArray(); + } + } + + protected override void OnParametersSet() + { + if (_subscribed == EditContext) return; + if (_subscribed != null) _subscribed.OnValidationStateChanged -= OnValidationStateChanged; + _subscribed = EditContext; + if (_subscribed != null) _subscribed.OnValidationStateChanged += OnValidationStateChanged; + } + + private void OnValidationStateChanged(object? sender, ValidationStateChangedEventArgs e) => InvokeAsync(StateHasChanged); + + public void Dispose() + { + if (_subscribed != null) _subscribed.OnValidationStateChanged -= OnValidationStateChanged; + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/FormTemplate.cs b/src/ShellUI.Templates/Templates/FormTemplate.cs index ff937a1..9c4decb 100644 --- a/src/ShellUI.Templates/Templates/FormTemplate.cs +++ b/src/ShellUI.Templates/Templates/FormTemplate.cs @@ -13,13 +13,13 @@ public class FormTemplate FilePath = "Form.razor", Tags = new List { "form", "validation", "input", "wrapper" }, - Dependencies = new List { "label", "input", "button" } + Dependencies = new List { "label", "input", "button", "form-field", "form-item", "form-label", "form-control", "form-description", "form-message" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI @using Microsoft.AspNetCore.Components.Forms -
      + @ChildContent
      @@ -43,11 +43,13 @@ @using Microsoft.AspNetCore.Components.Forms [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + // For validation, use EditForm with DataAnnotationsValidator; FormField and FormMessage work inside it. private async Task HandleSubmit() { - await Task.CompletedTask; + if (OnValidSubmit.HasDelegate) await OnValidSubmit.InvokeAsync(); } } + "; } diff --git a/src/ShellUI.Templates/Templates/MenubarContentTemplate.cs b/src/ShellUI.Templates/Templates/MenubarContentTemplate.cs new file mode 100644 index 0000000..087cdb8 --- /dev/null +++ b/src/ShellUI.Templates/Templates/MenubarContentTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class MenubarContentTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "menubar-content", + DisplayName = "Menubar Content", + Description = "Popover content for the compositional Menubar pattern", + Category = ComponentCategory.Navigation, + FilePath = "MenubarContent.razor", + IsAvailable = false, + Tags = new List { "navigation", "menubar", "content" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@if (Parent?.IsOpen == true) +{ +
      + @ChildContent +
      +} + +@code { + [CascadingParameter] private MenubarMenu? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { 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 index d5c02fc..bc56c44 100644 --- a/src/ShellUI.Templates/Templates/MenubarItemTemplate.cs +++ b/src/ShellUI.Templates/Templates/MenubarItemTemplate.cs @@ -17,33 +17,69 @@ public class MenubarItemTemplate public static string Content => @"@namespace YourProjectNamespace.Components.UI - +@if (!string.IsNullOrEmpty(Title)) +{ +
      + -@code { - [Parameter] - public RenderFragment? ChildContent { get; set; } - - [Parameter] - public EventCallback OnClick { get; set; } - - [Parameter] - public bool Disabled { get; set; } - - [Parameter] - public string? Class { get; set; } + @if (IsOpen) + { +
      + @ChildContent +
      + } +
      + + @if (IsOpen) + { +
      + } +} +else +{ + +} +@code { + [CascadingParameter] private MenubarMenu? Menu { get; set; } + // Set for the dropdown form; leave empty for a clickable item. + [Parameter] public string Title { get; set; } = """"; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public bool Disabled { 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? AdditionalAttributes { get; set; } + + private bool IsOpen { get; set; } + + private string ItemClass => Shell.Cn( + Menu != null + ? ""relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"" + : ""inline-flex items-center rounded-sm px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"", + ""disabled:pointer-events-none disabled:opacity-50"", + ClassName, Class); + + private void Toggle() => IsOpen = !IsOpen; + private void Close() => IsOpen = false; + + private async Task Select(MouseEventArgs e) + { + await OnClick.InvokeAsync(e); + Menu?.Close(); + } } "; } diff --git a/src/ShellUI.Templates/Templates/MenubarMenuTemplate.cs b/src/ShellUI.Templates/Templates/MenubarMenuTemplate.cs new file mode 100644 index 0000000..84f853f --- /dev/null +++ b/src/ShellUI.Templates/Templates/MenubarMenuTemplate.cs @@ -0,0 +1,58 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class MenubarMenuTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "menubar-menu", + DisplayName = "Menubar Menu", + Description = "Submenu container for the compositional Menubar pattern", + Category = ComponentCategory.Navigation, + FilePath = "MenubarMenu.razor", + IsAvailable = false, + Tags = new List { "navigation", "menubar", "menu" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + +
      + @ChildContent +
      +
      + +@if (IsOpen) +{ +
      +} + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + public bool IsOpen { get; private set; } + + public void Toggle() + { + IsOpen = !IsOpen; + StateHasChanged(); + } + + public void Open() + { + IsOpen = true; + StateHasChanged(); + } + + public void Close() + { + IsOpen = false; + StateHasChanged(); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/MenubarSeparatorTemplate.cs b/src/ShellUI.Templates/Templates/MenubarSeparatorTemplate.cs new file mode 100644 index 0000000..1423138 --- /dev/null +++ b/src/ShellUI.Templates/Templates/MenubarSeparatorTemplate.cs @@ -0,0 +1,28 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class MenubarSeparatorTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "menubar-separator", + DisplayName = "Menubar Separator", + Description = "Horizontal divider for the Menubar dropdown", + Category = ComponentCategory.Navigation, + FilePath = "MenubarSeparator.razor", + IsAvailable = false, + Tags = new List { "navigation", "menubar", "separator" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      + +@code { + [Parameter] public string? Class { 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 index e5a250e..2b93f91 100644 --- a/src/ShellUI.Templates/Templates/MenubarTemplate.cs +++ b/src/ShellUI.Templates/Templates/MenubarTemplate.cs @@ -13,7 +13,7 @@ public class MenubarTemplate FilePath = "Menubar.razor", Tags = new List { "navigation", "menu", "menubar", "app" }, - Dependencies = new List { "menubar-item" } + Dependencies = new List { "menubar-item", "menubar-menu", "menubar-trigger", "menubar-content", "menubar-separator" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI diff --git a/src/ShellUI.Templates/Templates/MenubarTriggerTemplate.cs b/src/ShellUI.Templates/Templates/MenubarTriggerTemplate.cs new file mode 100644 index 0000000..6e7eb95 --- /dev/null +++ b/src/ShellUI.Templates/Templates/MenubarTriggerTemplate.cs @@ -0,0 +1,38 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class MenubarTriggerTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "menubar-trigger", + DisplayName = "Menubar Trigger", + Description = "Trigger button for the compositional Menubar pattern", + Category = ComponentCategory.Navigation, + FilePath = "MenubarTrigger.razor", + IsAvailable = false, + Tags = new List { "navigation", "menubar", "trigger" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + +@code { + [CascadingParameter] private MenubarMenu? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private void Toggle() => Parent?.Toggle(); +} +"; +} diff --git a/src/ShellUI.Templates/Templates/PaginationContentTemplate.cs b/src/ShellUI.Templates/Templates/PaginationContentTemplate.cs new file mode 100644 index 0000000..10571ff --- /dev/null +++ b/src/ShellUI.Templates/Templates/PaginationContentTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PaginationContentTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "pagination-content", + DisplayName = "Pagination Content", + Description = "List wrapper (
        ) for the compositional Pagination pattern", + Category = ComponentCategory.Navigation, + FilePath = "PaginationContent.razor", + IsAvailable = false, + Tags = new List { "navigation", "pagination", "content" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
          + @ChildContent +
        + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/PaginationEllipsisTemplate.cs b/src/ShellUI.Templates/Templates/PaginationEllipsisTemplate.cs new file mode 100644 index 0000000..ebe253b --- /dev/null +++ b/src/ShellUI.Templates/Templates/PaginationEllipsisTemplate.cs @@ -0,0 +1,35 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PaginationEllipsisTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "pagination-ellipsis", + DisplayName = "Pagination Ellipsis", + Description = "Ellipsis placeholder between page numbers in the compositional Pagination pattern", + Category = ComponentCategory.Navigation, + FilePath = "PaginationEllipsis.razor", + IsAvailable = false, + Tags = new List { "navigation", "pagination", "ellipsis" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + + + + + + More pages + + +@code { + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/PaginationItemTemplate.cs b/src/ShellUI.Templates/Templates/PaginationItemTemplate.cs new file mode 100644 index 0000000..e1f27ea --- /dev/null +++ b/src/ShellUI.Templates/Templates/PaginationItemTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PaginationItemTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "pagination-item", + DisplayName = "Pagination Item", + Description = "List item (
      • ) wrapper for the compositional Pagination pattern", + Category = ComponentCategory.Navigation, + FilePath = "PaginationItem.razor", + IsAvailable = false, + Tags = new List { "navigation", "pagination", "item" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
      • + @ChildContent +
      • + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/PaginationLinkTemplate.cs b/src/ShellUI.Templates/Templates/PaginationLinkTemplate.cs new file mode 100644 index 0000000..700497e --- /dev/null +++ b/src/ShellUI.Templates/Templates/PaginationLinkTemplate.cs @@ -0,0 +1,59 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PaginationLinkTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "pagination-link", + DisplayName = "Pagination Link", + Description = "Page-number link/button for the compositional Pagination pattern", + Category = ComponentCategory.Navigation, + FilePath = "PaginationLink.razor", + IsAvailable = false, + Tags = new List { "navigation", "pagination", "link" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +@if (!string.IsNullOrEmpty(Href)) +{ + + @ChildContent + +} +else +{ + +} + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public bool IsActive { get; set; } + [Parameter] public string Size { get; set; } = ""icon""; + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private string ComposedClass => Shell.Cn( + ""inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"", + Size == ""icon"" ? ""h-9 w-9"" : ""h-9 px-4 py-2"", + IsActive + ? ""border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground"" + : ""hover:bg-accent hover:text-accent-foreground"", + Class); +} +"; +} diff --git a/src/ShellUI.Templates/Templates/PaginationNextTemplate.cs b/src/ShellUI.Templates/Templates/PaginationNextTemplate.cs new file mode 100644 index 0000000..c6b207b --- /dev/null +++ b/src/ShellUI.Templates/Templates/PaginationNextTemplate.cs @@ -0,0 +1,35 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PaginationNextTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "pagination-next", + DisplayName = "Pagination Next", + Description = "Next-page button for the compositional Pagination pattern", + Category = ComponentCategory.Navigation, + FilePath = "PaginationNext.razor", + IsAvailable = false, + Tags = new List { "navigation", "pagination", "next" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + Next + + + + + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/PaginationPreviousTemplate.cs b/src/ShellUI.Templates/Templates/PaginationPreviousTemplate.cs new file mode 100644 index 0000000..b16e005 --- /dev/null +++ b/src/ShellUI.Templates/Templates/PaginationPreviousTemplate.cs @@ -0,0 +1,35 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class PaginationPreviousTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "pagination-previous", + DisplayName = "Pagination Previous", + Description = "Previous-page button for the compositional Pagination pattern", + Category = ComponentCategory.Navigation, + FilePath = "PaginationPrevious.razor", + IsAvailable = false, + Tags = new List { "navigation", "pagination", "previous" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + + + + + + Previous + + +@code { + [Parameter] public string? Href { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter] public EventCallback OnClick { 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 index 49f9ee1..9008b0e 100644 --- a/src/ShellUI.Templates/Templates/PaginationTemplate.cs +++ b/src/ShellUI.Templates/Templates/PaginationTemplate.cs @@ -11,50 +11,58 @@ public class PaginationTemplate Description = "Pagination component for navigating pages", Category = ComponentCategory.Navigation, FilePath = "Pagination.razor", + Dependencies = new List { "pagination-content", "pagination-item", "pagination-link", "pagination-previous", "pagination-next", "pagination-ellipsis" }, Tags = new List { "navigation", "pagination", "pages" } }; public static string Content => @"@namespace YourProjectNamespace.Components.UI - @code { @@ -66,6 +74,10 @@ public class PaginationTemplate [Parameter] public EventCallback OnPageChange { get; set; } + + // Set to build the pagination from PaginationContent, PaginationItem and PaginationLink. + [Parameter] + public RenderFragment? ChildContent { get; set; } [Parameter] public string? Class { get; set; } diff --git a/src/ShellUI.Templates/Templates/SheetCloseTemplate.cs b/src/ShellUI.Templates/Templates/SheetCloseTemplate.cs new file mode 100644 index 0000000..842f363 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SheetCloseTemplate.cs @@ -0,0 +1,36 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class SheetCloseTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sheet-close", + DisplayName = "Sheet Close", + Description = "Close button subcomponent for the compositional Sheet pattern", + Category = ComponentCategory.Overlay, + FilePath = "SheetClose.razor", + IsAvailable = false, + Tags = new List { "overlay", "sheet", "close" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
        + @ChildContent +
        + +@code { + [CascadingParameter] private Sheet? Parent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + private async Task Close() + { + if (Parent != null) await Parent.SetOpen(false); + } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SheetDescriptionTemplate.cs b/src/ShellUI.Templates/Templates/SheetDescriptionTemplate.cs new file mode 100644 index 0000000..c30942d --- /dev/null +++ b/src/ShellUI.Templates/Templates/SheetDescriptionTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class SheetDescriptionTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sheet-description", + DisplayName = "Sheet Description", + Description = "Description subcomponent for the compositional Sheet pattern", + Category = ComponentCategory.Overlay, + FilePath = "SheetDescription.razor", + IsAvailable = false, + Tags = new List { "overlay", "sheet", "description" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +

        + @ChildContent +

        + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SheetFooterTemplate.cs b/src/ShellUI.Templates/Templates/SheetFooterTemplate.cs new file mode 100644 index 0000000..a677117 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SheetFooterTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class SheetFooterTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sheet-footer", + DisplayName = "Sheet Footer", + Description = "Footer subcomponent for the compositional Sheet pattern", + Category = ComponentCategory.Overlay, + FilePath = "SheetFooter.razor", + IsAvailable = false, + Tags = new List { "overlay", "sheet", "footer" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
        + @ChildContent +
        + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/SheetHeaderTemplate.cs b/src/ShellUI.Templates/Templates/SheetHeaderTemplate.cs new file mode 100644 index 0000000..182c2ce --- /dev/null +++ b/src/ShellUI.Templates/Templates/SheetHeaderTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class SheetHeaderTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sheet-header", + DisplayName = "Sheet Header", + Description = "Header subcomponent for the compositional Sheet pattern", + Category = ComponentCategory.Overlay, + FilePath = "SheetHeader.razor", + IsAvailable = false, + Tags = new List { "overlay", "sheet", "header" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +
        + @ChildContent +
        + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { 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 index 5c7dbf1..653f78c 100644 --- a/src/ShellUI.Templates/Templates/SheetTemplate.cs +++ b/src/ShellUI.Templates/Templates/SheetTemplate.cs @@ -11,7 +11,7 @@ public class SheetTemplate Description = "Side panel/drawer component with multiple positions — supports compositional subcomponent pattern", Category = ComponentCategory.Overlay, FilePath = "Sheet.razor", - Dependencies = new List { "sheet-variants", "sheet-trigger", "sheet-content" }, + Dependencies = new List { "sheet-variants", "sheet-trigger", "sheet-content", "sheet-header", "sheet-title", "sheet-description", "sheet-footer", "sheet-close" }, Tags = new List { "overlay", "sheet", "drawer", "panel", "side" } }; diff --git a/src/ShellUI.Templates/Templates/SheetTitleTemplate.cs b/src/ShellUI.Templates/Templates/SheetTitleTemplate.cs new file mode 100644 index 0000000..6f64ca4 --- /dev/null +++ b/src/ShellUI.Templates/Templates/SheetTitleTemplate.cs @@ -0,0 +1,31 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public class SheetTitleTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "sheet-title", + DisplayName = "Sheet Title", + Description = "Title subcomponent for the compositional Sheet pattern", + Category = ComponentCategory.Overlay, + FilePath = "SheetTitle.razor", + IsAvailable = false, + Tags = new List { "overlay", "sheet", "title" } + }; + + public static string Content => @"@namespace YourProjectNamespace.Components.UI + +

        + @ChildContent +

        + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } +} +"; +} From fdf56539221f48050a64525cfdc332a13856dc54 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 1 Oct 2026 15:10:16 +0200 Subject: [PATCH 5/7] feat: expand ComponentRegistry with new templates and update documentation - Added multiple new templates to the ComponentRegistry, including Breadcrumb, Form, Menubar, Pagination, Sheet, Drawer, AlertDialog, and Command components, enhancing the overall component library. - Updated the README to reflect the new total of 239 entries in the registry, including an increase in hidden dependency entries from 104 to 149. - Each new template includes necessary metadata and content structure for improved integration within the ShellUI framework. --- src/ShellUI.Templates/ComponentRegistry.cs | 90 ++++++++++++++++++++++ src/ShellUI.Templates/README.md | 4 +- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index e55ff41..8ece03d 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -86,6 +86,11 @@ public static class ComponentRegistry { "accordion-trigger", AccordionTriggerTemplate.Metadata }, { "accordion-content", AccordionContentTemplate.Metadata }, { "breadcrumb", BreadcrumbTemplate.Metadata }, + { "breadcrumb-ellipsis", BreadcrumbEllipsisTemplate.Metadata }, + { "breadcrumb-separator", BreadcrumbSeparatorTemplate.Metadata }, + { "breadcrumb-page", BreadcrumbPageTemplate.Metadata }, + { "breadcrumb-link", BreadcrumbLinkTemplate.Metadata }, + { "breadcrumb-list", BreadcrumbListTemplate.Metadata }, { "breadcrumb-item", BreadcrumbItemTemplate.Metadata }, { "toast", ToastTemplate.Metadata }, { "sonner", SonnerTemplate.Metadata }, @@ -107,6 +112,12 @@ public static class ComponentRegistry { "table-head", TableHeadTemplate.Metadata }, { "table-cell", TableCellTemplate.Metadata }, { "form", FormTemplate.Metadata }, + { "form-message", FormMessageTemplate.Metadata }, + { "form-description", FormDescriptionTemplate.Metadata }, + { "form-control", FormControlTemplate.Metadata }, + { "form-label", FormLabelTemplate.Metadata }, + { "form-item", FormItemTemplate.Metadata }, + { "form-field", FormFieldTemplate.Metadata }, { "input-otp", InputOTPTemplate.Metadata }, { "combobox", ComboboxTemplate.Metadata }, { "date-picker", DatePickerTemplate.Metadata }, @@ -119,15 +130,35 @@ public static class ComponentRegistry { "nav-content", NavContentTemplate.Metadata }, { "navigation-menu-item", NavigationMenuItemTemplate.Metadata }, { "menubar", MenubarTemplate.Metadata }, + { "menubar-separator", MenubarSeparatorTemplate.Metadata }, + { "menubar-content", MenubarContentTemplate.Metadata }, + { "menubar-trigger", MenubarTriggerTemplate.Metadata }, + { "menubar-menu", MenubarMenuTemplate.Metadata }, { "menubar-item", MenubarItemTemplate.Metadata }, { "pagination", PaginationTemplate.Metadata }, + { "pagination-ellipsis", PaginationEllipsisTemplate.Metadata }, + { "pagination-next", PaginationNextTemplate.Metadata }, + { "pagination-previous", PaginationPreviousTemplate.Metadata }, + { "pagination-link", PaginationLinkTemplate.Metadata }, + { "pagination-item", PaginationItemTemplate.Metadata }, + { "pagination-content", PaginationContentTemplate.Metadata }, { "scroll-area", ScrollAreaTemplate.Metadata }, { "resizable", ResizableTemplate.Metadata }, { "sheet", SheetTemplate.Metadata }, + { "sheet-close", SheetCloseTemplate.Metadata }, + { "sheet-footer", SheetFooterTemplate.Metadata }, + { "sheet-description", SheetDescriptionTemplate.Metadata }, + { "sheet-title", SheetTitleTemplate.Metadata }, + { "sheet-header", SheetHeaderTemplate.Metadata }, { "sheet-trigger", SheetTriggerTemplate.Metadata }, { "sheet-content", SheetContentTemplate.Metadata }, { "sheet-variants", SheetVariantsTemplate.Metadata }, { "drawer", DrawerTemplate.Metadata }, + { "drawer-close", DrawerCloseTemplate.Metadata }, + { "drawer-footer", DrawerFooterTemplate.Metadata }, + { "drawer-description", DrawerDescriptionTemplate.Metadata }, + { "drawer-title", DrawerTitleTemplate.Metadata }, + { "drawer-header", DrawerHeaderTemplate.Metadata }, { "drawer-trigger", DrawerTriggerTemplate.Metadata }, { "drawer-content", DrawerContentTemplate.Metadata }, { "drawer-variants", DrawerVariantsTemplate.Metadata }, @@ -137,12 +168,26 @@ public static class ComponentRegistry { "data-table", DataTableTemplate.Metadata }, { "data-table-models", DataTableModelsTemplate.Metadata }, { "alert-dialog", AlertDialogTemplate.Metadata }, + { "alert-dialog-cancel", AlertDialogCancelTemplate.Metadata }, + { "alert-dialog-action", AlertDialogActionTemplate.Metadata }, + { "alert-dialog-footer", AlertDialogFooterTemplate.Metadata }, + { "alert-dialog-description", AlertDialogDescriptionTemplate.Metadata }, + { "alert-dialog-title", AlertDialogTitleTemplate.Metadata }, + { "alert-dialog-header", AlertDialogHeaderTemplate.Metadata }, + { "alert-dialog-content", AlertDialogContentTemplate.Metadata }, + { "alert-dialog-trigger", AlertDialogTriggerTemplate.Metadata }, { "calendar", CalendarTemplate.Metadata }, { "loading", LoadingTemplate.Metadata }, { "hover-card", HoverCardTemplate.Metadata }, { "hover-card-trigger", HoverCardTriggerTemplate.Metadata }, { "hover-card-content", HoverCardContentTemplate.Metadata }, { "command", CommandTemplate.Metadata }, + { "command-separator", CommandSeparatorTemplate.Metadata }, + { "command-empty", CommandEmptyTemplate.Metadata }, + { "command-option", CommandOptionTemplate.Metadata }, + { "command-group", CommandGroupTemplate.Metadata }, + { "command-list", CommandListTemplate.Metadata }, + { "command-input", CommandInputTemplate.Metadata }, { "command-palette", CommandPaletteTemplate.Metadata }, { "data-picker", DataPickerTemplate.Metadata }, { "multi-select", MultiSelectTemplate.Metadata }, @@ -286,6 +331,11 @@ public static class ComponentRegistry "accordion-trigger" => AccordionTriggerTemplate.Content, "accordion-content" => AccordionContentTemplate.Content, "breadcrumb" => BreadcrumbTemplate.Content, + "breadcrumb-ellipsis" => BreadcrumbEllipsisTemplate.Content, + "breadcrumb-separator" => BreadcrumbSeparatorTemplate.Content, + "breadcrumb-page" => BreadcrumbPageTemplate.Content, + "breadcrumb-link" => BreadcrumbLinkTemplate.Content, + "breadcrumb-list" => BreadcrumbListTemplate.Content, "breadcrumb-item" => BreadcrumbItemTemplate.Content, "toast" => ToastTemplate.Content, "sonner" => SonnerTemplate.Content, @@ -307,6 +357,12 @@ public static class ComponentRegistry "table-head" => TableHeadTemplate.Content, "table-cell" => TableCellTemplate.Content, "form" => FormTemplate.Content, + "form-message" => FormMessageTemplate.Content, + "form-description" => FormDescriptionTemplate.Content, + "form-control" => FormControlTemplate.Content, + "form-label" => FormLabelTemplate.Content, + "form-item" => FormItemTemplate.Content, + "form-field" => FormFieldTemplate.Content, "input-otp" => InputOTPTemplate.Content, "combobox" => ComboboxTemplate.Content, "date-picker" => DatePickerTemplate.Content, @@ -319,15 +375,35 @@ public static class ComponentRegistry "nav-content" => NavContentTemplate.Content, "navigation-menu-item" => NavigationMenuItemTemplate.Content, "menubar" => MenubarTemplate.Content, + "menubar-separator" => MenubarSeparatorTemplate.Content, + "menubar-content" => MenubarContentTemplate.Content, + "menubar-trigger" => MenubarTriggerTemplate.Content, + "menubar-menu" => MenubarMenuTemplate.Content, "menubar-item" => MenubarItemTemplate.Content, "pagination" => PaginationTemplate.Content, + "pagination-ellipsis" => PaginationEllipsisTemplate.Content, + "pagination-next" => PaginationNextTemplate.Content, + "pagination-previous" => PaginationPreviousTemplate.Content, + "pagination-link" => PaginationLinkTemplate.Content, + "pagination-item" => PaginationItemTemplate.Content, + "pagination-content" => PaginationContentTemplate.Content, "scroll-area" => ScrollAreaTemplate.Content, "resizable" => ResizableTemplate.Content, "sheet" => SheetTemplate.Content, + "sheet-close" => SheetCloseTemplate.Content, + "sheet-footer" => SheetFooterTemplate.Content, + "sheet-description" => SheetDescriptionTemplate.Content, + "sheet-title" => SheetTitleTemplate.Content, + "sheet-header" => SheetHeaderTemplate.Content, "sheet-trigger" => SheetTriggerTemplate.Content, "sheet-content" => SheetContentTemplate.Content, "sheet-variants" => SheetVariantsTemplate.Content, "drawer" => DrawerTemplate.Content, + "drawer-close" => DrawerCloseTemplate.Content, + "drawer-footer" => DrawerFooterTemplate.Content, + "drawer-description" => DrawerDescriptionTemplate.Content, + "drawer-title" => DrawerTitleTemplate.Content, + "drawer-header" => DrawerHeaderTemplate.Content, "drawer-trigger" => DrawerTriggerTemplate.Content, "drawer-content" => DrawerContentTemplate.Content, "drawer-variants" => DrawerVariantsTemplate.Content, @@ -337,12 +413,26 @@ public static class ComponentRegistry "data-table" => DataTableTemplate.Content, "data-table-models" => DataTableModelsTemplate.Content, "alert-dialog" => AlertDialogTemplate.Content, + "alert-dialog-cancel" => AlertDialogCancelTemplate.Content, + "alert-dialog-action" => AlertDialogActionTemplate.Content, + "alert-dialog-footer" => AlertDialogFooterTemplate.Content, + "alert-dialog-description" => AlertDialogDescriptionTemplate.Content, + "alert-dialog-title" => AlertDialogTitleTemplate.Content, + "alert-dialog-header" => AlertDialogHeaderTemplate.Content, + "alert-dialog-content" => AlertDialogContentTemplate.Content, + "alert-dialog-trigger" => AlertDialogTriggerTemplate.Content, "calendar" => CalendarTemplate.Content, "loading" => LoadingTemplate.Content, "hover-card" => HoverCardTemplate.Content, "hover-card-trigger" => HoverCardTriggerTemplate.Content, "hover-card-content" => HoverCardContentTemplate.Content, "command" => CommandTemplate.Content, + "command-separator" => CommandSeparatorTemplate.Content, + "command-empty" => CommandEmptyTemplate.Content, + "command-option" => CommandOptionTemplate.Content, + "command-group" => CommandGroupTemplate.Content, + "command-list" => CommandListTemplate.Content, + "command-input" => CommandInputTemplate.Content, "command-palette" => CommandPaletteTemplate.Content, "data-picker" => DataPickerTemplate.Content, "multi-select" => MultiSelectTemplate.Content, diff --git a/src/ShellUI.Templates/README.md b/src/ShellUI.Templates/README.md index fee70f4..a9b2b30 100644 --- a/src/ShellUI.Templates/README.md +++ b/src/ShellUI.Templates/README.md @@ -32,10 +32,10 @@ The global tool command is `shellui`; a local .NET tool is invoked as `dotnet sh ## Template inventory -The registry contains **194 entries**: +The registry contains **239 entries**: - **90 direct install targets** displayed by `shellui list` -- **104 hidden dependency entries** installed recursively and omitted from the direct list +- **149 hidden dependency entries** installed recursively and omitted from the direct list Each entry provides content plus metadata used by the CLI, including its display name, category, description, target path, version, dependencies, and optional NuGet dependencies. The catalog also includes hidden subcomponents, variants, models, services, JavaScript, and stylesheet assets required by direct targets. From f350b7b25119f7819640ee65d99c2286f72b1a47 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 1 Oct 2026 15:11:05 +0200 Subject: [PATCH 6/7] docs: update README to reflect new ComponentRegistry entry counts - Updated the ComponentRegistry entry count from 194 to 239, with hidden dependency entries increasing from 104 to 149. - Adjusted documentation to ensure accuracy regarding the current state of components and dependencies. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db9db07..30b6fbd 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ ShellUI is pre-1.0, so APIs and generated output can still change between minor ## Capabilities - The CLI commands are `init`, `add`, `list`, `remove`, and `update`, plus `theme init`, `theme apply`, and `theme update`. -- The component registry has **194 entries**: **90 direct install targets** and **104 hidden dependency entries**. `list` shows direct targets; `add` resolves hidden dependencies. +- The component registry has **239 entries**: **90 direct install targets** and **149 hidden dependency entries**. `list` shows direct targets; `add` resolves hidden dependencies. - `0.3.0` added `typed-select`, `command-palette`, `data-picker`, `multi-select`, `tag-input`, `donut-chart`, `radar-chart`, and `radial-chart`. - `ShellUI.Components` supports a release-generated precompiled CSS bundle and a generated safelist for existing Tailwind builds. - The CLI can install source with Tailwind's standalone executable or an npm-based build. The current Tailwind baseline is `4.3.2`. From 360c14a3321d9e6f1dd7f3f2af57a1baee4dfd2b Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 2 Oct 2026 09:12:50 +0200 Subject: [PATCH 7/7] chore: remove redundant comments and keep only the necessary ones --- .gitattributes | 62 ---------------- .github/FUNDING.yml | 1 - .github/workflows/ci.yml | 70 +++---------------- .github/workflows/release.yml | 9 +-- .gitignore | 5 +- Directory.Build.props | 4 +- .../BlazorInteractiveServer.csproj | 5 -- .../Components/App.razor | 11 +-- .../Components/Demo/AdvancedFormDemo.razor | 8 --- .../Components/Demo/ButtonsDemo.razor | 1 - .../Demo/ComplexComponentsDemo.razor | 18 ----- .../Components/Demo/DataDisplayDemo.razor | 5 -- .../Components/Demo/FormControlsDemo.razor | 8 --- .../Components/Demo/NavigationDemo.razor | 6 -- .../Components/Demo/OverlayDemo.razor | 7 -- .../Components/Layout/DashboardLayout.razor | 7 +- .../Components/Layout/MainLayout.razor | 17 ----- .../Components/Layout/MainLayout.razor.css | 1 - .../Components/Models/DataTableModels.cs | 3 +- .../Components/Pages/Blocks.razor | 2 - .../Components/Pages/Dashboard.razor | 2 - .../Components/Pages/Home.razor | 6 -- .../Components/UI/AccordionType.cs | 4 +- .../Components/UI/AppSidebar.razor | 15 +--- .../Components/UI/Calendar.razor | 3 - .../Components/UI/Chart.razor | 2 - .../Components/UI/Combobox.razor | 3 +- .../Components/UI/DashboardDemoSidebar.razor | 4 +- .../Components/UI/DataPicker.razor | 6 +- .../Components/UI/DataTable.razor | 16 +---- .../Components/UI/DatePicker.razor | 4 +- .../Components/UI/DateRangePicker.razor | 3 +- .../Components/UI/Form.razor | 1 - .../Components/UI/InputOTP.razor | 1 - .../Components/UI/MultiSelect.razor | 3 +- .../Components/UI/ShellUILogo.razor | 1 - .../Components/UI/Sidebar.razor | 16 +---- .../Components/UI/SidebarProvider.razor | 1 - .../Components/UI/Stepper.razor | 1 - .../Components/UI/ThemeToggle.razor | 3 +- .../Components/UI/TypedSelect.razor | 7 +- .../Components/UI/Variants/ChartVariants.cs | 18 ++--- NET10/BlazorInteractiveServer/Program.cs | 3 - .../wwwroot/css/charts.css | 10 +-- .../BlazorInteractiveServer/wwwroot/input.css | 20 ++---- .../wwwroot/shellui-sidebar.js | 6 +- .../wwwroot/shellui.js | 8 +-- ShellUI.Tests/ChartStylesTests.cs | 7 +- ShellUI.Tests/InitBootstrapTests.cs | 9 +-- ShellUI.Tests/IntegrationTest1.cs | 15 ++-- ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs | 42 ++--------- ShellUI.Tests/SafelistDriftTests.cs | 19 ++--- ShellUI.Tests/TemplateCompileTests.cs | 25 ++----- ShellUI.Tests/TemplateSyncTests.cs | 15 +--- ShellUI.Tests/ThemeServiceTests.cs | 2 +- ShellUI.Tests/UnitTest1.cs | 3 - prepare-release.ps1 | 7 -- scripts/rebuild-precompiled-css.sh | 23 ++---- src/ShellUI.CLI/Program.cs | 3 +- .../Services/ComponentInstaller.cs | 18 +---- src/ShellUI.CLI/Services/InitService.cs | 49 ++----------- src/ShellUI.CLI/Services/ProjectDetector.cs | 2 - .../Services/TailwindDownloader.cs | 1 - src/ShellUI.CLI/ShellUI.CLI.csproj | 1 - src/ShellUI.Components/AccordionType.cs | 4 +- .../Components/AppSidebar.razor | 12 ---- .../Components/Calendar.razor | 3 - src/ShellUI.Components/Components/Chart.razor | 6 +- .../Components/Combobox.razor | 3 +- .../Components/DataPicker.razor | 6 +- .../Components/DataTable.razor | 16 +---- .../Components/DatePicker.razor | 4 +- .../Components/DateRangePicker.razor | 3 +- .../Components/InputOTP.razor | 1 - .../Components/MultiSelect.razor | 3 +- .../Components/ShellUILogo.razor | 1 - .../Components/Sidebar.razor | 16 +---- .../Components/SidebarProvider.razor | 1 - .../Components/Stepper.razor | 1 - .../Components/StepperStep.razor | 4 +- .../Components/ThemeToggle.razor | 3 +- .../Components/TypedSelect.razor | 7 +- .../Models/DataTableModels.cs | 3 +- .../ShellUI.Components.csproj | 8 +-- .../Variants/ChartVariants.cs | 18 ++--- .../wwwroot/shellui-sidebar.js | 6 +- .../wwwroot/shellui-theme.css | 14 +--- src/ShellUI.Components/wwwroot/shellui.js | 11 +-- src/ShellUI.Core/Models/ComponentMetadata.cs | 12 +--- src/ShellUI.Core/TailwindConstants.cs | 4 +- src/ShellUI.Templates/Build/ShellUI.targets | 5 +- src/ShellUI.Templates/ComponentRegistry.cs | 4 +- src/ShellUI.Templates/CssTemplates.cs | 17 +---- .../Templates/AccordionTypeTemplate.cs | 4 +- .../Templates/CalendarTemplate.cs | 3 - .../Templates/CardHeaderTemplate.cs | 2 +- .../Templates/ChartStylesTemplate.cs | 13 +--- .../Templates/ChartTemplate.cs | 2 - .../Templates/ChartVariantsTemplate.cs | 18 ++--- .../Templates/ComboboxTemplate.cs | 3 +- .../Templates/DataPickerTemplate.cs | 3 +- .../Templates/DataTableTemplate.cs | 12 +--- .../Templates/DatePickerTemplate.cs | 3 +- .../Templates/DateRangePickerTemplate.cs | 3 +- .../Templates/InputOTPTemplate.cs | 2 - .../Templates/MultiSelectTemplate.cs | 3 +- .../Templates/ShellUIJsTemplate.cs | 8 +-- .../Templates/SidebarJsTemplate.cs | 3 +- .../Templates/SidebarProviderTemplate.cs | 1 - .../Templates/SidebarTemplate.cs | 12 +--- .../Templates/ThemeToggleTemplate.cs | 6 +- src/ShellUI.Templates/VersionHelper.cs | 5 +- tools/ShellUI.SafelistGenerator/Program.cs | 8 +-- 113 files changed, 161 insertions(+), 807 deletions(-) diff --git a/.gitattributes b/.gitattributes index ef82812..efdba87 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,64 +1,2 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### * text=auto *.sh text eol=lf - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index aa22b9d..e69de29 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +0,0 @@ -# These are supported funding model platforms diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22f45cd..05e5d09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,22 +30,15 @@ jobs: - name: Restore dependencies run: dotnet restore ShellUI.slnx - # Generates src/ShellUI.Components/wwwroot/shellui-all.css fresh on every run. - # File is gitignored — the Windows + Linux Tailwind CLI binaries produce - # byte-different output, so drift-checking a committed file failed spuriously. - # Instead we regenerate and validate content properties. + # Regenerated every run: it's gitignored because Windows and Linux Tailwind builds differ byte for byte. - name: Build precompiled CSS bundle - # Invoke via `bash` (not `./script.sh`) so we don't rely on the file's - # executable bit. Windows contributors can edit and commit without - # remembering `git update-index --chmod=+x`. + # Run through bash so the executable bit doesn't matter. run: | set -euxo pipefail bash scripts/rebuild-precompiled-css.sh BUNDLE=src/ShellUI.Components/wwwroot/shellui-all.css test -f "$BUNDLE" || (echo "ERROR: bundle was not produced"; exit 1) - # Content assertions using grep -F (fixed-string, no regex escaping headaches). - # These catch classes of bugs we've actually shipped before — worth the paranoia. grep -Fq -- '--background' "$BUNDLE" || (echo "ERROR: theme vars not in bundle"; exit 1) grep -Fq '.bg-background' "$BUNDLE" || (echo "ERROR: core utility rule missing"; exit 1) grep -Fq 'hover\:bg-accent' "$BUNDLE" || (echo "ERROR: hover: modifier rules missing"; exit 1) @@ -53,8 +46,7 @@ jobs: grep -Fq 'px-2\.5' "$BUNDLE" || (echo "ERROR: Badge padding class px-2.5 missing (variant .cs helpers not scanned?)"; exit 1) grep -Fq 'border-transparent' "$BUNDLE" || (echo "ERROR: border-transparent missing (variant .cs helpers not scanned?)"; exit 1) - # Size guard — keep the threshold above the current component bundle, - # but below runaway output (for example, an unminified full Tailwind base). + # Above the current bundle, below an accidental full Tailwind build. size=$(wc -c < "$BUNDLE") echo "precompiled bundle size: ${size} bytes" if [ "$size" -gt 150000 ]; then @@ -68,8 +60,7 @@ jobs: - name: Run tests run: dotnet test ShellUI.slnx --no-restore --no-build --configuration Release --verbosity normal - # End-to-end build of a scaffolded project — catches anything the in-process - # TemplateCompileTests miss (missing usings, dependency resolution). + # Builds a scaffolded project; catches what TemplateCompileTests can't. - name: Smoke-test CLI scaffolding shell: bash run: | @@ -84,8 +75,6 @@ jobs: cd SmokeApp shellui init --tailwind standalone --yes - # Assert init produced a working host: App.razor patched with render mode, - # theme bootstrap, and shellui.js script tag. grep -q 'HeadOutlet @rendermode="InteractiveServer"' Components/App.razor || (echo "init did not patch HeadOutlet @rendermode"; exit 1) grep -q 'Routes @rendermode="InteractiveServer"' Components/App.razor || (echo "init did not patch Routes @rendermode"; exit 1) grep -q 'ShellUI theme bootstrap' Components/App.razor || (echo "init did not inject theme bootstrap"; exit 1) @@ -95,32 +84,22 @@ jobs: exit 1 fi - # Assert input.css has the full theme, not just @import "tailwindcss"; grep -q '@theme inline' wwwroot/input.css || (echo "init did not write full theme to input.css"; exit 1) grep -q ':root' wwwroot/input.css || (echo "init did not write :root variables to input.css"; exit 1) grep -q '\.dark' wwwroot/input.css || (echo "init did not write .dark variables to input.css"; exit 1) shellui add chart pie-chart dashboard-02 data-table --force - # NuGet dependencies (Blazor-ApexCharts, System.Linq.Dynamic.Core) should - # now be added automatically by `shellui add`. Assert they appear in the - # project file so a regression in the auto-install fails loudly here. grep -q 'Blazor-ApexCharts' SmokeApp.csproj || (echo "shellui add chart did not add Blazor-ApexCharts NuGet dep"; exit 1) grep -q 'System.Linq.Dynamic.Core' SmokeApp.csproj || (echo "shellui add data-table did not add System.Linq.Dynamic.Core NuGet dep"; exit 1) - # And the DataTable models file must land at Components/UI/Models/, not be missing. test -f Components/UI/Models/DataTableModels.cs || (echo "shellui add data-table did not install data-table-models"; exit 1) - # chart-styles ships the CSS for the custom tooltip + ApexCharts chrome. - # Without it, hovering a chart shows invisible white-on-white text. + # Without charts.css, chart tooltips render white on white. test -f wwwroot/css/charts.css || (echo "shellui add chart did not install chart-styles CSS"; exit 1) grep -q '/, not the root. + # A dynamic import of shellui-sidebar.js 404s once compiled into a consumer's RCL. grep -q 'ShellUI.initSidebar' Components/UI/SidebarProvider.razor || (echo "SidebarProvider did not call ShellUI.initSidebar"; exit 1) if grep -Fq 'shellui-sidebar.js' Components/UI/SidebarProvider.razor; then echo "SidebarProvider still dynamically imports shellui-sidebar.js" @@ -134,8 +113,6 @@ jobs: dotnet build -c Debug - # init --dashboard wires the layout: Routes default, stock MainLayout/NavMenu removed, - # error bar moved to App.razor, sidebar links built from the app's pages. cd "$TMPDIR/app" dotnet new blazor -o DashApp --no-restore cd DashApp @@ -147,8 +124,7 @@ jobs: grep -q '"/counter"' Components/UI/AppSidebar.razor || (echo "AppSidebar links were not built from the pages"; exit 1) dotnet build -c Debug - # Every direct target must install and compile together; this catches missing - # dependencies and template compile errors that per-template tests can't see. + # Catches missing dependencies that per-template tests can't see. ALL=$(python3 - "$GITHUB_WORKSPACE" <<'PY' import os, re, sys root = sys.argv[1] @@ -168,12 +144,7 @@ jobs: dotnet build -c Debug done - # Pure-NuGet install path — `dotnet add package ShellUI.Components` without - # the CLI. Uses a one-off NuGet.config that whitelists ONLY the local feed, - # so a missing local package can't silently fall back to nuget.org and pull - # an older version (which would mask whether the current PR's package is - # correct). $GITHUB_WORKSPACE is absolute — relative paths from inside the - # temp consumer dir would resolve to the wrong root. + # Package-only install from a local-only feed, so nuget.org can't mask a broken package. - name: NuGet-only install — verify safelist ships shell: bash run: | @@ -182,8 +153,6 @@ jobs: test -d "$LOCAL_FEED" || (echo "local feed not found at $LOCAL_FEED — did the pack step run?"; exit 1) ls "$LOCAL_FEED"/*.nupkg | head -3 - # DIAGNOSTIC: dump what's actually inside the fresh nupkg. The precompiled - # bundle must ship as a static web asset for consumers to `` it. echo "---nupkg contents (looking for shellui-all.css):---" unzip -l "$LOCAL_FEED"/ShellUI.Components*.nupkg | grep -E 'shellui-all|staticwebassets/' | head -10 || echo " (no matches — bundle not in nupkg!)" echo "---source wwwroot check:---" @@ -194,8 +163,6 @@ jobs: dotnet new blazor -o NuGetSmoke --no-restore cd NuGetSmoke - # Pin restore to only the local feed; nuget.org explicitly disabled so a - # broken local feed can't be papered over by a public-published older version. cat > NuGet.config < @@ -206,19 +173,12 @@ jobs: EOF - # Evict every cached ShellUI.Components from ~/.nuget/packages before restore. - # The runner-level cache is restored from actions/cache and may contain a - # stale copy of the current version from an earlier CI run (before we added - # the precompiled bundle, .cs scan, etc.). NuGet prefers cached over local - # feeds when version matches, so a stale cache silently masks the fresh - # nupkg. Wildcard covers any version — future-proof. + # A cached copy of the same version would shadow the fresh nupkg. rm -rf ~/.nuget/packages/shellui.components dotnet add package ShellUI.Components --prerelease - # The safelist gets copied into wwwroot/ during the consumer's build by - # the package's build/ShellUI.Components.targets — auto-imported by NuGet. - # Need to actually build the project, not just restore, for the Copy to run. + # The package's .targets copies the safelist during build, so restore alone isn't enough. dotnet build -c Debug SAFELIST="wwwroot/shellui-classes.txt" @@ -229,16 +189,10 @@ jobs: exit 1 ) - # Sanity: the safelist contains real Tailwind classes grep -q 'bg-background' "$SAFELIST" || (echo "safelist appears malformed — missing core Tailwind class 'bg-background'"; exit 1) wc -l "$SAFELIST" - # In .NET 10, MapStaticAssets() doesn't physically copy static web assets - # from referenced packages into the consumer's bin/ during `dotnet build` - # — it uses a manifest and reads from the extracted NuGet cache at runtime. - # So the correct assertion is "does the file exist in the extracted package - # cache after restore", which proves it'll be served at - # `_content/ShellUI.Components/shellui-all.css` in the running app. + # .NET 10 serves package assets from the NuGet cache, not bin/, so check the extracted package. BUNDLE="$(find ~/.nuget/packages/shellui.components -name 'shellui-all.css' -type f | head -1)" if [ -z "$BUNDLE" ]; then echo "ERROR: shellui-all.css not in consumer's extracted NuGet cache — the pure-NuGet install path is broken." @@ -250,8 +204,6 @@ jobs: BUNDLE_SIZE=$(wc -c < "$BUNDLE") echo "bundle size: $BUNDLE_SIZE bytes" - # Spot-check a handful of representative rules using fixed-string grep - # (no regex escaping headaches). These catch classes of bugs we've hit. grep -Fq -- '--background' "$BUNDLE" || (echo "ERROR: bundle missing --background theme var"; exit 1) grep -Fq '.bg-background' "$BUNDLE" || (echo "ERROR: bundle missing .bg-background utility"; exit 1) grep -Fq 'hover\:bg-accent' "$BUNDLE" || (echo "ERROR: bundle missing hover:bg-accent modifier rule"; exit 1) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa558cb..e1644d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,15 +53,14 @@ jobs: - name: Restore dependencies run: dotnet restore ShellUI.slnx - # Generate the precompiled CSS bundle before build so releases always ship - # a fresh one. File is gitignored — regenerated on every CI/release run. + # shellui-all.css is gitignored, so every release regenerates it. - name: Build precompiled CSS bundle run: | set -euxo pipefail bash scripts/rebuild-precompiled-css.sh BUNDLE=src/ShellUI.Components/wwwroot/shellui-all.css test -f "$BUNDLE" || (echo "ERROR: bundle was not produced"; exit 1) - # Content sanity — same assertions ci.yml runs. + # Same checks as ci.yml. grep -q -- '--background:' "$BUNDLE" || (echo "ERROR: theme vars missing"; exit 1) grep -q '\.bg-background' "$BUNDLE" || (echo "ERROR: utilities missing"; exit 1) echo "release bundle size: $(wc -c < "$BUNDLE") bytes" @@ -75,9 +74,7 @@ jobs: - name: Pack NuGet packages run: dotnet pack ShellUI.slnx --no-build --configuration Release -p:ContinuousIntegrationBuild=true - # ShellUI.Core and ShellUI.Templates are internal-only — they have no public API - # surface that consumers reference. Only the CLI tool and the Components runtime - # library are pushed to NuGet. + # Core and Templates are internal; only the CLI and Components are published. - name: Publish to NuGet run: | dotnet nuget push src/ShellUI.CLI/bin/Release/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.gitignore b/.gitignore index db285ba..3cdd1a9 100644 --- a/.gitignore +++ b/.gitignore @@ -366,8 +366,5 @@ shellui-installation-tests/ # Preview publish output (testing artifacts) preview-publish/ -# Generated Tailwind bundle — regenerated in CI, not committed. -# Windows and Linux Tailwind CLI produce byte-different output; committing -# would cause spurious drift-check failures. Local devs: run -# `bash scripts/rebuild-precompiled-css.sh` before running Path A tests. +# Regenerated in CI; run scripts/rebuild-precompiled-css.sh locally. src/ShellUI.Components/wwwroot/shellui-all.css diff --git a/Directory.Build.props b/Directory.Build.props index 9a0221f..9699b1c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,12 +1,11 @@ - + 0.4.0 alpha.1 - $(ShellUIVersion) $(ShellUIVersion)-$(ShellUIVersionSuffix) @@ -15,7 +14,6 @@ $(Version) - ShellUI ShellUI diff --git a/NET10/BlazorInteractiveServer/BlazorInteractiveServer.csproj b/NET10/BlazorInteractiveServer/BlazorInteractiveServer.csproj index 4e61c5e..13d9939 100644 --- a/NET10/BlazorInteractiveServer/BlazorInteractiveServer.csproj +++ b/NET10/BlazorInteractiveServer/BlazorInteractiveServer.csproj @@ -12,11 +12,6 @@ - - - diff --git a/NET10/BlazorInteractiveServer/Components/App.razor b/NET10/BlazorInteractiveServer/Components/App.razor index 7246aae..0df9e21 100644 --- a/NET10/BlazorInteractiveServer/Components/App.razor +++ b/NET10/BlazorInteractiveServer/Components/App.razor @@ -6,29 +6,23 @@ - - - - - "; var result = InitService.RewriteAppRazor(bare); @@ -108,8 +105,6 @@ public void RewriteAppRazor_IsIdempotent() [Fact] public void RewriteAppRazor_PreservesExistingRenderMode() { - // If the user (or another tool) already set a different render mode (e.g. Auto), - // we must not overwrite it. const string custom = @"" + "\n" + @""; diff --git a/ShellUI.Tests/IntegrationTest1.cs b/ShellUI.Tests/IntegrationTest1.cs index 15e185b..4c5163d 100644 --- a/ShellUI.Tests/IntegrationTest1.cs +++ b/ShellUI.Tests/IntegrationTest1.cs @@ -17,29 +17,23 @@ public CliIntegrationTests() [Fact] public async Task InitCommand_DoesNotCrash() { - // Create a minimal Blazor project await CreateTestBlazorProject(); - // Run init command with --yes flag (just test it doesn't crash) var exitCode = await RunShellUICommand("init --yes"); - // We expect it to work, but if it fails due to missing dependencies in test env, that's ok - // The important thing is it doesn't crash the process - Assert.True(exitCode == 0 || exitCode == 1); // 0 = success, 1 = expected failure in test env + // init may fail in the test environment; it only must not crash. + Assert.True(exitCode == 0 || exitCode == 1); } [Fact] public async Task AddCommand_DoesNotCrash() { - // Create a minimal Blazor project await CreateTestBlazorProject(); - // Test: Add button component (just test it doesn't crash) var exitCode = await RunShellUICommand("add button"); - // We expect it to work, but if it fails due to missing dependencies in test env, that's ok - // The important thing is it doesn't crash the process - Assert.True(exitCode == 0 || exitCode == 1); // 0 = success, 1 = expected failure in test env + // add may fail in the test environment; it only must not crash. + Assert.True(exitCode == 0 || exitCode == 1); } private async Task CreateTestBlazorProject() @@ -64,7 +58,6 @@ private async Task CreateTestBlazorProject() private async Task RunShellUICommand(string args) { - // Get the path to the CLI from the test directory perspective var solutionDir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..")); var cliPath = Path.Combine(solutionDir, "src", "ShellUI.CLI", "bin", "Release", "net10.0", "ShellUI.CLI.dll"); diff --git a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs index 15673a9..f64fc46 100644 --- a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs +++ b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs @@ -22,8 +22,6 @@ public void FindClosestMatch_SuggestsExpectedComponent(string typo, string expec [Fact] public void FindClosestMatch_ReturnsNullForExactMatch() { - // Exact matches have distance 0 and are excluded from suggestions — - // the caller already knows the component exists. Assert.Null(ComponentRegistry.FindClosestMatch("button")); } @@ -36,8 +34,7 @@ public void FindClosestMatch_ReturnsNullWhenNothingIsClose() [Fact] public void FindClosestMatch_DoesNotSuggestHiddenSubComponents() { - // `data-table-models` is IsAvailable=false (installed only as a dep of data-table). - // A user typo like "data-table-modls" should not be redirected to it. + // Hidden entries are never suggested. var result = ComponentRegistry.FindClosestMatch("data-table-modls"); Assert.NotEqual("data-table-models", result); } @@ -80,9 +77,7 @@ public void Chart_DeclaresBlazorApexCharts() [InlineData("multi-series-chart")] public void ChartVariants_TransitivelyPullInBlazorApexCharts(string componentName) { - // Chart family components don't declare the NuGet dep themselves — they depend - // on `chart` which does. Verify the dependency chain is intact so the installer's - // recursive walk picks up the package. + // Chart types get the NuGet package through their `chart` dependency. var metadata = ComponentRegistry.GetMetadata(componentName); Assert.NotNull(metadata); Assert.Contains("chart", metadata!.Dependencies); @@ -91,10 +86,7 @@ public void ChartVariants_TransitivelyPullInBlazorApexCharts(string componentNam public class DependencyGraphTests { - // Every dependency name declared by any registered component must itself resolve - // via ComponentRegistry.GetMetadata. Catches the class of bug where a sub-component - // template exists on disk but is never wired into the registry — the CLI then - // reports "Failed: " and the consumer project won't compile. + // An unregistered dependency makes `shellui add` report "Failed: ". [Fact] public void EveryDeclaredDependency_ResolvesInRegistry() { @@ -112,10 +104,7 @@ public void EveryDeclaredDependency_ResolvesInRegistry() string.Join("\n ", missing)); } - // Every installable component (IsAvailable = true) must route through the - // GetComponentContent switch. Sub-component stubs registered as IsAvailable = false - // with intentionally-empty Content are allowed — they exist only so the CLI can - // resolve them as dependency names when the parent renders their markup inline. + // Hidden parts may have empty Content when the parent renders their markup. [Fact] public void EveryInstallableComponent_HasContentMapping() { @@ -135,13 +124,7 @@ public void EveryInstallableComponent_HasContentMapping() public class HiddenNetworkDependencyTests { - // ShellUI never links Google Material Symbols/Icons or Font Awesome and every - // component uses inline SVG for its chrome. When a template depends on an - // external icon font the raw icon name ("expand_more", "cloud_upload", ...) - // shows as literal text or the glyph slot renders empty on consumers. - // Fail loudly here so the SVG-swap convention stays enforced across the whole - // registry — the specific classes below cover both Material families and - // Font Awesome's solid/regular/brands. + // Consumers don't load icon fonts, so the icon name would render as text. [Theory] [InlineData("material-symbols-outlined")] // Material Symbols (newer variable font) [InlineData("material-symbols-rounded")] @@ -167,15 +150,7 @@ public void NoTemplate_DependsOnExternalIconFont(string cssClass) public class RelativeJsModuleImportTests { - // A component whose C# does JSRuntime.InvokeAsync("import", "./foo.js") resolves that - // path against the current page URL. That only works when ShellUI is installed straight - // into the host app; the moment the generated component is compiled into a consumer's - // own Razor Class Library, the asset is served from _content// instead and the - // import 404s — silently, since every one of these calls is wrapped in try/catch. - // ShellUI's established fix for this shape of bug (see ThemeToggle, InputOTP, - // CommandPalette, Combobox, ...) is to route through the already-loaded global - // `window.ShellUI` object (shellui.js, loaded via one host-controlled immediately before blazor.web.js — provides - // window.ShellUI.* (addClassToDocument, focusElement, copyToClipboard, …) for - // ThemeToggle, CopyButton, InputOTP, FileUpload, Command. The pattern matches - // both the modern `@Assets["_framework/blazor.web.js"]` and the bare form. + // shellui.js must load before blazor.web.js; matches both @Assets[...] and the bare form. if (!content.Contains("shellui.js")) { content = Regex.Replace( @@ -557,8 +524,6 @@ internal static string RewriteWasmIndexHtml(string content) return content; } - // Idempotent injection of a stylesheet before . Used by post-install - // hooks when a component ships a CSS asset (e.g. chart-styles → css/charts.css). internal static string InjectStylesheetLink(string content, string href) { var linkTag = $""; @@ -569,8 +534,6 @@ internal static string InjectStylesheetLink(string content, string href) return Regex.Replace(content, @"", $" {linkTag}\n", RegexOptions.IgnoreCase); } - // Used by ComponentInstaller after `shellui add` so newly-installed CSS assets - // are wired into the host without requiring the user to edit App.razor by hand. internal static async Task InjectStylesheetIntoHostAsync(string href) { var cwd = Directory.GetCurrentDirectory(); @@ -611,7 +574,6 @@ private static void RemoveBootstrapFiles() AnsiConsole.MarkupLine("[cyan]Checking for Bootstrap files to clean up...[/]"); var deletedCount = 0; - // 1. Delete wwwroot/lib/bootstrap (.NET 9+) and wwwroot/bootstrap (.NET 8) foreach (var folder in new[] { Path.Combine("lib", "bootstrap"), "bootstrap" }) { var bootstrapDir = Path.Combine(wwwrootPath, folder); @@ -621,7 +583,6 @@ private static void RemoveBootstrapFiles() deletedCount++; } - // 2. Delete bootstrap css files in wwwroot/css var cssPath = Path.Combine(wwwrootPath, "css"); if (Directory.Exists(cssPath)) { diff --git a/src/ShellUI.CLI/Services/ProjectDetector.cs b/src/ShellUI.CLI/Services/ProjectDetector.cs index fc15eb1..1c1cbb8 100644 --- a/src/ShellUI.CLI/Services/ProjectDetector.cs +++ b/src/ShellUI.CLI/Services/ProjectDetector.cs @@ -36,7 +36,6 @@ 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)) { @@ -52,7 +51,6 @@ private static ProjectType DetectProjectType(XDocument doc, string csprojPath) return ProjectType.BlazorServerSideRendering; } - // Fallback: check SDK var sdk = doc.Root?.Attribute("Sdk")?.Value ?? ""; if (sdk.Contains("Web")) return ProjectType.BlazorServerSideRendering; diff --git a/src/ShellUI.CLI/Services/TailwindDownloader.cs b/src/ShellUI.CLI/Services/TailwindDownloader.cs index 4a789b0..2c7ef6e 100644 --- a/src/ShellUI.CLI/Services/TailwindDownloader.cs +++ b/src/ShellUI.CLI/Services/TailwindDownloader.cs @@ -48,7 +48,6 @@ private static async Task DownloadTailwindCliAsync(string destinationPath) 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}"); diff --git a/src/ShellUI.CLI/ShellUI.CLI.csproj b/src/ShellUI.CLI/ShellUI.CLI.csproj index b1f13c8..cbeaba9 100644 --- a/src/ShellUI.CLI/ShellUI.CLI.csproj +++ b/src/ShellUI.CLI/ShellUI.CLI.csproj @@ -16,7 +16,6 @@ enable enable - true shellui ShellUI.CLI diff --git a/src/ShellUI.Components/AccordionType.cs b/src/ShellUI.Components/AccordionType.cs index e704d64..38a38e3 100644 --- a/src/ShellUI.Components/AccordionType.cs +++ b/src/ShellUI.Components/AccordionType.cs @@ -2,6 +2,6 @@ namespace ShellUI.Components; public enum AccordionType { - Single, // one open at a time - Multiple // multiple open + Single, + Multiple } diff --git a/src/ShellUI.Components/Components/AppSidebar.razor b/src/ShellUI.Components/Components/AppSidebar.razor index 25693e5..85f6f2d 100644 --- a/src/ShellUI.Components/Components/AppSidebar.razor +++ b/src/ShellUI.Components/Components/AppSidebar.razor @@ -3,7 +3,6 @@ @* AppSidebar - Inset sidebar. Usage: SidebarProvider > AppSidebar + SidebarInset. Collapsible: Offcanvas or Icon. *@ - @* Header: Logo & Company *@ @@ -18,10 +17,8 @@ - @* Content *@ - @* NavMain: Platform *@ Platform @@ -59,7 +56,6 @@ - @* NavProjects *@ Projects @@ -87,7 +83,6 @@ - @* NavSecondary (pushed to bottom) *@ @@ -106,7 +101,6 @@ - @* Footer: User Profile *@ @@ -118,7 +112,6 @@ data-size="lg" title="Shewart" class="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 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:!justify-center group-data-[collapsible=icon]:!items-center"> - @* Icon + expandable arrows when collapsed *@