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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
*.sh text eol=lf

###############################################################################
# Set default behavior for command prompt diff.
Expand Down
23 changes: 18 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,28 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

- name: Extract release notes for this version
run: bash scripts/extract-release-notes.sh "${{ steps.version.outputs.VERSION }}" > "$RUNNER_TEMP/release-body.md"

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Check package version matches tag
run: |
for proj in src/ShellUI.CLI/ShellUI.CLI.csproj src/ShellUI.Components/ShellUI.Components.csproj; do
v=$(dotnet msbuild "$proj" -getProperty:Version)
if [ "$v" != "${{ steps.version.outputs.VERSION }}" ]; then
echo "::error::$proj builds version $v but the tag is v${{ steps.version.outputs.VERSION }}. Update Directory.Build.props."
exit 1
fi
done

- name: Cache NuGet packages
uses: actions/cache@v4
with:
Expand Down Expand Up @@ -72,15 +89,11 @@ jobs:
dotnet nuget push src/ShellUI.Components/bin/Release/*.snupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
fi

- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: ShellUI v${{ steps.version.outputs.VERSION }}
body_path: docs/RELEASE_NOTES.md
body_path: ${{ runner.temp }}/release-body.md
draft: false
prerelease: ${{ contains(github.ref, '-') }}
files: |
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<Project>
<!-- Centralized ShellUI Version - Update this single file to version all components -->
<PropertyGroup>
<ShellUIVersion>0.4.0</ShellUIVersion>
<ShellUIVersionSuffix>alpha.1</ShellUIVersionSuffix>
<ShellUIVersion>0.3.0</ShellUIVersion>
<ShellUIVersionSuffix>rc.2</ShellUIVersionSuffix>
</PropertyGroup>

<!-- Common properties for all ShellUI projects -->
Expand Down
11 changes: 11 additions & 0 deletions NET10/BlazorInteractiveServer/Components/UI/RadarChart.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,15 @@
[Parameter] public string Name { get; set; } = "Data";
[Parameter] public Func<TItem, object>? XValue { get; set; }
[Parameter] public Func<TItem, decimal?>? YValue { get; set; }

protected override void OnParametersSet()
{
base.OnParametersSet();
// Radar tooltips only trigger per point, not in shared mode.
if (ChartOptions.Tooltip != null)
{
ChartOptions.Tooltip.Shared = false;
ChartOptions.Tooltip.Intersect = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,11 @@ public static ApexCharts.ApexChartOptions<TItem> GetOptions<TItem>(ChartTheme th
// Resolve x-axis label from the first source that has a real string.
// ApexCharts stashes category names in different globals depending on axis type.
function xLabelAt(i) {
const cats = (w.globals.categoryLabels && w.globals.categoryLabels.length ? w.globals.categoryLabels
: (w.config.xaxis && w.config.xaxis.categories) || w.globals.labels || []);
const v = cats[i];
return v !== undefined && v !== null ? String(v) : '';
const sources = [w.globals.categoryLabels, w.config.xaxis && w.config.xaxis.categories, w.globals.labels];
for (const s of sources) {
if (s && s[i] !== undefined && s[i] !== null && s[i] !== '') return String(s[i]);
}
return '';
}

let html = '<div class=""shellui-chart-tooltip"">';
Expand Down
55 changes: 20 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,25 @@

| Channel | Version | Notes |
|---|---|---|
| Repository source | `0.4.0-alpha.1` | Current source; targets .NET 10 and Tailwind CSS `4.3.2` |
| Latest published stable packages | `0.2.1` | Stable CLI and components packages on NuGet |
| Latest published prerelease packages | `0.3.0-rc.1` | Published release candidate; older than this source checkout |
| Latest prerelease (recommended) | `0.3.0-rc.2` | .NET 10, Tailwind CSS `4.3.2` |
| Latest stable | `0.2.1` | Older release; superseded once `0.3.0` ships |

The repository is ahead of NuGet. Features described as **current source** require a local build or package and are not present in the published `0.2.1` or `0.3.0-rc.1` packages. The published prerelease targets .NET 9; the current source targets .NET 10.
Prereleases are not picked up by a plain install, so pass `--version 0.3.0-rc.2` explicitly. Projects still on .NET 9 can use `0.3.0-rc.1`, the last release that targets .NET 9.

ShellUI is alpha software. Validate it in your target Blazor and hosting environments before relying on it.
ShellUI is prerelease software. Validate it in your target Blazor and hosting environments before relying on it.

## Current source capabilities
## Capabilities

- The CLI commands are `init`, `add`, `list`, `remove`, and `update`, plus `theme init`, `theme apply`, and `theme update`.
- The component registry has **173 entries**: **73 direct install targets** and **100 hidden dependency entries**. `list` shows direct targets; `add` resolves hidden dependencies.
- Current additions include `typed-select`, `command-palette`, `data-picker`, `multi-select`, and `tag-input`.
- The component registry has **176 entries**: **76 direct install targets** and **100 hidden dependency entries**. `list` shows direct targets; `add` resolves hidden dependencies.
- `0.3.0-rc.2` adds `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`.
- The repository and demo have migrated to .NET 10. The demo is `NET10/BlazorInteractiveServer`.

## Requirements

- .NET 10 SDK for the current source
- .NET 10 SDK
- A .NET 10 Blazor project
- Tailwind CSS `4.3.2` via either:
- the standalone CLI, which does not require Node.js; or
Expand All @@ -54,25 +53,19 @@ ShellUI is alpha software. Validate it in your target Blazor and hosting environ
The published global tool is named `shellui`:

```bash
dotnet tool install --global ShellUI.CLI --version 0.2.1
dotnet tool install --global ShellUI.CLI --version 0.3.0-rc.2
shellui --help
```

To select the published prerelease instead:

```bash
dotnet tool install --global ShellUI.CLI --version 0.3.0-rc.1
```

A local .NET tool is invoked as `dotnet shellui`:

```bash
dotnet new tool-manifest
dotnet tool install --local ShellUI.CLI --version 0.3.0-rc.1
dotnet tool install --local ShellUI.CLI --version 0.3.0-rc.2
dotnet shellui --help
```

After building or installing the current source tool, its workflow is:
The workflow is:

```bash
shellui init
Expand All @@ -99,29 +92,21 @@ New CLI sidebar installs use the host-loaded `shellui.js`; the legacy `sidebar-j
| `theme apply <url-or-id>` | Apply a theme to `wwwroot/input.css` or emit override CSS |
| `theme update` | Re-fetch the source recorded in `shellui.theme.lock` |

The five current source targets can be installed together:
The targets added in `0.3.0-rc.2` can be installed together:

```bash
shellui add typed-select command-palette data-picker multi-select tag-input
shellui add typed-select command-palette data-picker multi-select tag-input donut-chart radar-chart radial-chart
```

## Components package

Published versions can be installed explicitly:

```bash
dotnet add package ShellUI.Components --version 0.2.1
```

To select the published prerelease instead:

```bash
dotnet add package ShellUI.Components --version 0.3.0-rc.1 --prerelease
dotnet add package ShellUI.Components --version 0.3.0-rc.2
```

`0.4.0-alpha.1` is not currently published. To consume that source version, pack `src/ShellUI.Components/ShellUI.Components.csproj` and use it from a local package feed. `ShellUI.Core` and `ShellUI.Templates` are internal projects and must not be installed by consumers.
`ShellUI.Core` and `ShellUI.Templates` are internal projects and must not be installed by consumers.

The current-source package supports two CSS workflows.
The package supports two CSS workflows.

### Precompiled bundle

Expand All @@ -135,7 +120,7 @@ Release packaging generates `shellui-all.css`, so the consuming project does not
@using ShellUI.Components
```

The generated bundle is not checked into the repository. Before packing the current source locally, run `bash scripts/rebuild-precompiled-css.sh`; the release pipeline runs the same script automatically.
The generated bundle is not checked into the repository. Before packing locally, run `bash scripts/rebuild-precompiled-css.sh`; the release pipeline runs the same script automatically.

### Tailwind safelist

Expand All @@ -150,7 +135,7 @@ The safelist lets Tailwind emit the classes used by ShellUI components even when

## Theme commands

The current-source CLI can fetch public themes from [tweakcn](https://tweakcn.com):
The CLI can fetch public themes from [tweakcn](https://tweakcn.com):

```bash
shellui theme init https://tweakcn.com/themes/THEME_ID
Expand All @@ -177,7 +162,7 @@ The .NET 10 Blazor Interactive Server demo is outside that solution:
dotnet run --project NET10/BlazorInteractiveServer/BlazorInteractiveServer.csproj
```

`Directory.Build.props` is the source of the centralized version, currently `0.4.0-alpha.1`. Only `ShellUI.CLI` and `ShellUI.Components` are packable; `ShellUI.Core`, `ShellUI.Templates`, the test project, and the safelist generator are not published packages.
`Directory.Build.props` holds the single version used by every project. Only `ShellUI.CLI` and `ShellUI.Components` are packable; `ShellUI.Core`, `ShellUI.Templates`, the test project, and the safelist generator are not published packages.

ShellUI uses semantic Blazor and Tailwind patterns, but accessibility depends on the component, configuration, and host application. Test keyboard use, focus behavior, contrast, and assistive-technology output in the consuming application.

Expand All @@ -189,7 +174,7 @@ ShellUI uses semantic Blazor and Tailwind patterns, but accessibility depends on
- [Contributing](docs/CONTRIBUTING.md)
- [Release notes](docs/RELEASE_NOTES.md)

Release notes contain historical release records plus a separate current-source section for `0.4.0-alpha.1`; older version sections describe only the release they document.
Each section of the release notes describes only the release it is named after.

## License

Expand Down
32 changes: 32 additions & 0 deletions SHELLUI-FIXES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ShellUI fixes

Problems hit while adding ShellUI 0.3.0-rc.1 components to `src/FDMS.UI` and how each was handled. The fixes now live in the current `0.4.0-alpha.1` source templates; they are not all present in the published `0.3.0-rc.1` package. The legacy `sidebar-js` module remains only for projects that still contain an old generated provider.

## Edits to generated components

1. `Tabs.razor` **does not compile.** The template emits `private string _effectiveValue = ";` (an unterminated string). Fixed to `= "";`.
2. `Select.razor` **depends on an icon font nothing loads.** It draws the chevron with `<span class="material-symbols-outlined">expand_more</span>`, which needs the Material Symbols font, so without it the arrow would render as the plain text "expand_more". Replaced with an inline SVG chevron.
3. `SidebarProvider.razor` **never detects mobile.** The old template imported `./shellui-sidebar.js`, which resolves against the page URL and fails when the component is compiled into a Razor class library. New CLI installs use the host-loaded `shellui.js` and call `ShellUI.initSidebar`; no library-specific path is required. The legacy `shellui-sidebar.js` module remains available only for projects that still use the old generated provider.
4. `SidebarInset.razor` **lets wide content push the page sideways.** The `<main>` is a flex child without `min-w-0`, so a wide table stretches it past the viewport instead of scrolling inside its own container. Added `min-w-0`.
5. `Skeleton.razor` **uses `ClassName` while the component API and demo use `Class`.** Calls such as `<Skeleton Class="h-4 w-full" />` therefore pass `Class` through unmatched attributes instead of merging it with the skeleton classes, which can remove the pulse/background styling. The component now exposes `Class`, retains `ClassName` for compatibility, and renders `ChildContent` like the ShellUI demo.



## Customised from the dashboard-02 template

- `Components/Layout/DashboardLayout02.razor`: breadcrumb labels, redirect to the account page while a password change is pending, `min-w-0` on the content area.
- `Components/UI/AppSidebar.razor`: FDMS navigation, admin-only Users link, account and sign-out entries.



## Things to know when using it

- **Class overrides do not merge.** `Shell.Cn` joins class strings and does not resolve conflicts like tailwind-merge does. Passing `Class="max-w-2xl"` to something that already sets `max-w-lg` or `sm:max-w-sm` is a coin toss. Use the important modifier, for example `sm:max-w-2xl!` (see `InvoiceDetailSheet.razor`).
- **Styles and scripts come from the library.** `FDMS.UI` builds `wwwroot/app.css` with the Tailwind CLI (`Build/ShellUI.targets`, needs Node). Hosts must reference `_content/FDMS.UI/app.css` and `_content/FDMS.UI/shellui.js` and must not copy the CSS into their own `wwwroot`.
- **Tailwind only scans** `FDMS.UI` **by default.** Classes used in a host's own markup (for example `FDMS.Web/Components`) are not generated unless the host is listed with `@source` in `wwwroot/input.css`.
- **Theme flash.** `ThemeToggle` assumes dark when nothing is stored, and it only touches the `dark` class after the first render. The host needs the small inline script in `FDMS.Web/Components/App.razor` that sets a default and adds `dark` before Blazor starts.
- **No prerender with browser storage.** The signed-in session lives in browser storage, so `FDMS.Web` renders `Routes` and `HeadOutlet` with `prerender: false`. `ThemeToggle` and `SidebarProvider` also need JS interop and skip it during prerender.
- `data-table` pulls in `System.Linq.Dynamic.Core`. It filters and sorts by property name, so columns need a real `PropertyName`.
- **Icons.** The `ShellIcons.Blazor` package (Lucide names, for example `<ShellIcon Name="refresh-cw" />`) is used for icons rather than inline SVGs.
- **Run the CLI from** `src/FDMS.UI`, next to `shellui.json`.

58 changes: 58 additions & 0 deletions ShellUI.Tests/ClassParameterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using ShellUI.Templates;
using Xunit;

namespace ShellUI.Tests;

public class ClassParameterTests
{
private static readonly Regex DeclaresClassName = new(@"public string\?? ClassName\b");
private static readonly Regex DeclaresClass = new(@"public string\? Class \{");
private static readonly Regex RendersClass = new(@"@Class\b|[,+]\s*Class\)");

[Fact]
public void EveryTemplateWithClassName_AlsoAcceptsAndRendersClass()
{
var offenders = new List<string>();
foreach (var (name, _) in ComponentRegistry.Components)
{
var content = ComponentRegistry.GetComponentContent(name);
if (content is null || !DeclaresClassName.IsMatch(content)) continue;

if (!DeclaresClass.IsMatch(content)) offenders.Add($"{name}: no `Class` parameter");
else if (!RendersClass.IsMatch(content)) offenders.Add($"{name}: `Class` declared but never rendered");
}

Assert.True(offenders.Count == 0,
"Templates that expose ClassName must also accept and render Class:\n " +
string.Join("\n ", offenders));
}

[Fact]
public void EveryPackageComponentWithClassName_AlsoAcceptsAndRendersClass()
{
var offenders = new List<string>();
foreach (var path in Directory.GetFiles(GetComponentsDirectory(), "*.razor"))
{
var content = File.ReadAllText(path);
if (!DeclaresClassName.IsMatch(content)) continue;

var file = Path.GetFileName(path);
if (!DeclaresClass.IsMatch(content)) offenders.Add($"{file}: no `Class` parameter");
else if (!RendersClass.IsMatch(content)) offenders.Add($"{file}: `Class` declared but never rendered");
}

Assert.True(offenders.Count == 0,
"Package components that expose ClassName must also accept and render Class:\n " +
string.Join("\n ", offenders));
}

private static string GetComponentsDirectory([CallerFilePath] string thisFile = "")
{
var testDir = Path.GetDirectoryName(thisFile) ?? throw new InvalidOperationException("CallerFilePath is empty");
return Path.GetFullPath(Path.Combine(testDir, "..", "src", "ShellUI.Components", "Components"));
}
}
3 changes: 3 additions & 0 deletions ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public void Chart_DeclaresBlazorApexCharts()

[Theory]
[InlineData("pie-chart")]
[InlineData("donut-chart")]
[InlineData("radar-chart")]
[InlineData("radial-chart")]
[InlineData("bar-chart")]
[InlineData("area-chart")]
[InlineData("line-chart")]
Expand Down
16 changes: 16 additions & 0 deletions ShellUI.Tests/TemplateSyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class TemplateSyncTests
[InlineData("multi-select", "MultiSelect.razor")]
[InlineData("tag-input", "TagInput.razor")]
[InlineData("command-palette", "CommandPalette.razor")]
[InlineData("donut-chart", "DonutChart.razor")]
[InlineData("radar-chart", "RadarChart.razor")]
[InlineData("radial-chart", "RadialChart.razor")]
public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string razorFileName)
{
if (AllowedDrift.ContainsKey(templateName)) return;
Expand All @@ -55,6 +58,19 @@ public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string raz
DiffSummary(normalizedLive, normalizedTemplate));
}

[Fact]
public void ChartVariantsTemplate_MatchesLiveLibrary()
{
var livePath = Path.Combine(Path.GetDirectoryName(GetLiveRazorPath("Chart.razor"))!, "..", "Variants", "ChartVariants.cs");
var template = ComponentRegistry.GetComponentContent("chart-variants")
?? throw new InvalidOperationException("Template 'chart-variants' not found in registry");

var live = Normalize(File.ReadAllText(livePath));
var generated = Normalize(template.Replace("namespace YourProjectNamespace.Components.UI.Variants;", "namespace ShellUI.Components;"));

Assert.True(live == generated, "Drift detected between live ChartVariants.cs and template chart-variants.\n\n" + DiffSummary(live, generated));
}

// [CallerFilePath] captures the absolute path of this source file at compile time,
// so the test resolves the live components directory regardless of cwd on CI.
private static string GetLiveRazorPath(string razorFileName, [CallerFilePath] string thisFile = "")
Expand Down
Loading
Loading