Add fluentui-blazor skill

Add a new skill for using the Microsoft Fluent UI Blazor component library (Microsoft.FluentUI.AspNetCore.Components v4) in Blazor applications. Includes guidance on setup, component usage, theming, data grids, layout and navigation.
This commit is contained in:
Adrien Clerbois
2026-02-16 22:43:41 +01:00
parent 7855e66af8
commit c824a3c8b6
6 changed files with 799 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
# FluentDataGrid
`FluentDataGrid<TGridItem>` is a strongly-typed generic component for displaying tabular data.
## Basic Usage
```razor
<FluentDataGrid Items="@people" TGridItem="Person">
<PropertyColumn Property="@(p => p.Name)" Sortable="true" />
<PropertyColumn Property="@(p => p.Email)" />
<PropertyColumn Property="@(p => p.BirthDate)" Format="yyyy-MM-dd" />
<TemplateColumn Title="Actions">
<FluentButton OnClick="@(() => Edit(context))">Edit</FluentButton>
</TemplateColumn>
</FluentDataGrid>
```
**Critical**: Columns are child components, NOT properties. Use `PropertyColumn`, `TemplateColumn`, and `SelectColumn` within the grid.
## Column Types
### PropertyColumn
Binds to a property expression. Auto-derives title from property name or `[Display]` attribute.
```razor
<PropertyColumn Property="@(p => p.Name)" Sortable="true" />
<PropertyColumn Property="@(p => p.Price)" Format="C2" Title="Unit Price" />
<PropertyColumn Property="@(p => p.Category)" Comparer="@StringComparer.OrdinalIgnoreCase" />
```
Parameters: `Property` (required), `Format`, `Title`, `Sortable`, `SortBy`, `Comparer`, `IsDefaultSortColumn`, `InitialSortDirection`, `Class`, `Tooltip`.
### TemplateColumn
Full custom rendering via render fragment. `context` is the `TGridItem`.
```razor
<TemplateColumn Title="Status" SortBy="@statusSort">
<FluentBadge Appearance="Appearance.Accent"
BackgroundColor="@(context.IsActive ? "green" : "red")">
@(context.IsActive ? "Active" : "Inactive")
</FluentBadge>
</TemplateColumn>
```
### SelectColumn
Checkbox selection column.
```razor
<SelectColumn TGridItem="Person"
SelectMode="DataGridSelectMode.Multiple"
@bind-SelectedItems="@selectedPeople" />
```
Modes: `DataGridSelectMode.Single`, `DataGridSelectMode.Multiple`.
## Data Sources
Two mutually exclusive approaches:
### In-memory (IQueryable)
```razor
<FluentDataGrid Items="@people.AsQueryable()" TGridItem="Person">
...
</FluentDataGrid>
```
### Server-side / Custom (ItemsProvider)
```razor
<FluentDataGrid ItemsProvider="@peopleProvider" TGridItem="Person">
...
</FluentDataGrid>
@code {
private GridItemsProvider<Person> peopleProvider = async request =>
{
var result = await PeopleService.GetPeopleAsync(
request.StartIndex,
request.Count ?? 50,
request.GetSortByProperties().FirstOrDefault());
return GridItemsProviderResult.From(result.Items, result.TotalCount);
};
}
```
### EF Core Adapter
```csharp
// Program.cs
builder.Services.AddDataGridEntityFrameworkAdapter();
```
```razor
<FluentDataGrid Items="@dbContext.People" TGridItem="Person">
...
</FluentDataGrid>
```
## Pagination
```razor
<FluentDataGrid Items="@people" Pagination="@pagination" TGridItem="Person">
...
</FluentDataGrid>
<FluentPaginator State="@pagination" />
@code {
private PaginationState pagination = new() { ItemsPerPage = 10 };
}
```
## Virtualization
For large datasets, enable virtualization:
```razor
<FluentDataGrid Items="@people" Virtualize="true" ItemSize="46" TGridItem="Person">
...
</FluentDataGrid>
```
`ItemSize` is the estimated row height in pixels (default varies). Important for scroll position calculations.
## Key Parameters
| Parameter | Type | Description |
|---|---|---|
| `Items` | `IQueryable<TGridItem>?` | In-memory data source |
| `ItemsProvider` | `GridItemsProvider<TGridItem>?` | Async data provider |
| `Pagination` | `PaginationState?` | Pagination state |
| `Virtualize` | `bool` | Enable virtualization |
| `ItemSize` | `float` | Estimated row height (px) |
| `ItemKey` | `Func<TGridItem, object>?` | Stable key for `@key` |
| `ResizableColumns` | `bool` | Enable column resize |
| `HeaderCellAsButtonWithMenu` | `bool` | Sortable header UI |
| `GridTemplateColumns` | `string?` | CSS grid-template-columns |
| `Loading` | `bool` | Show loading indicator |
| `ShowHover` | `bool` | Highlight rows on hover |
| `OnRowClick` | `EventCallback<FluentDataGridRow<TGridItem>>` | Row click handler |
| `OnRowDoubleClick` | `EventCallback<FluentDataGridRow<TGridItem>>` | Row double-click handler |
| `OnRowFocus` | `EventCallback<FluentDataGridRow<TGridItem>>` | Row focus handler |
## Sorting
```razor
<PropertyColumn Property="@(p => p.Name)" Sortable="true" IsDefaultSortColumn="true"
InitialSortDirection="SortDirection.Ascending" />
```
Or with a custom sort:
```razor
<TemplateColumn Title="Full Name" SortBy="@(GridSort<Person>.ByAscending(p => p.LastName).ThenAscending(p => p.FirstName))">
@context.LastName, @context.FirstName
</TemplateColumn>
```

View File

@@ -0,0 +1,173 @@
# Layout and Navigation
## Layout Components
### FluentLayout
Root layout container. Use as the outermost structural component.
```razor
<FluentLayout Orientation="Orientation.Vertical">
<FluentHeader>...</FluentHeader>
<FluentBodyContent>...</FluentBodyContent>
<FluentFooter>...</FluentFooter>
</FluentLayout>
```
### FluentHeader / FluentFooter
Sticky header and footer sections within `FluentLayout`.
```razor
<FluentHeader Height="50">
<FluentStack Orientation="Orientation.Horizontal" HorizontalAlignment="HorizontalAlignment.SpaceBetween">
<span>App Title</span>
<FluentButton>Settings</FluentButton>
</FluentStack>
</FluentHeader>
```
### FluentBodyContent
Main scrollable content area within `FluentLayout`.
### FluentStack
Flexbox container for horizontal or vertical layouts.
```razor
<FluentStack Orientation="Orientation.Horizontal"
HorizontalGap="10"
VerticalGap="10"
HorizontalAlignment="HorizontalAlignment.Center"
VerticalAlignment="VerticalAlignment.Center"
Wrap="true"
Width="100%">
<FluentButton>One</FluentButton>
<FluentButton>Two</FluentButton>
</FluentStack>
```
Parameters: `Orientation`, `HorizontalGap`, `VerticalGap`, `HorizontalAlignment`, `VerticalAlignment`, `Wrap`, `Width`.
### FluentGrid / FluentGridItem
12-column responsive grid system.
```razor
<FluentGrid Spacing="3" Justify="JustifyContent.Center" AdaptiveRendering="true">
<FluentGridItem xs="12" sm="6" md="4" lg="3">
Card 1
</FluentGridItem>
<FluentGridItem xs="12" sm="6" md="4" lg="3">
Card 2
</FluentGridItem>
</FluentGrid>
```
Size parameters (`xs`, `sm`, `md`, `lg`, `xl`, `xxl`) represent column spans out of 12. Use `AdaptiveRendering="true"` to hide items that don't fit.
### FluentMainLayout (convenience)
Pre-composed layout with header, nav menu, and body area.
```razor
<FluentMainLayout Header="@header"
SubHeader="@subheader"
NavMenuContent="@navMenu"
Body="@body"
HeaderHeight="50"
NavMenuWidth="250"
NavMenuTitle="Navigation" />
```
## Navigation Components
### FluentNavMenu
Collapsible navigation menu with keyboard support.
```razor
<FluentNavMenu Width="250"
Collapsible="true"
@bind-Expanded="@menuExpanded"
Title="Main navigation"
CollapsedChildNavigation="true"
Margin="4px 0">
<FluentNavLink Href="/" Icon="@(Icons.Regular.Size20.Home)" Match="NavLinkMatch.All">
Home
</FluentNavLink>
<FluentNavLink Href="/counter" Icon="@(Icons.Regular.Size20.NumberSymbol)">
Counter
</FluentNavLink>
<FluentNavGroup Title="Admin" Icon="@(Icons.Regular.Size20.Shield)" @bind-Expanded="@adminExpanded">
<FluentNavLink Href="/admin/users">Users</FluentNavLink>
<FluentNavLink Href="/admin/roles">Roles</FluentNavLink>
</FluentNavGroup>
</FluentNavMenu>
```
Key parameters:
- `Width` — width in pixels (40px when collapsed)
- `Collapsible` — enables expand/collapse toggle
- `Expanded` / `ExpandedChanged` — bindable collapse state
- `CollapsedChildNavigation` — shows flyout menus for groups when collapsed
- `CustomToggle` — for mobile hamburger button patterns
- `Title` — aria-label for accessibility
### FluentNavGroup
Expandable group within a nav menu.
```razor
<FluentNavGroup Title="Settings"
Icon="@(Icons.Regular.Size20.Settings)"
@bind-Expanded="@settingsExpanded"
Gap="2">
<FluentNavLink Href="/settings/general">General</FluentNavLink>
<FluentNavLink Href="/settings/profile">Profile</FluentNavLink>
</FluentNavGroup>
```
Parameters: `Title`, `Expanded`/`ExpandedChanged`, `Icon`, `IconColor`, `HideExpander`, `Gap`, `MaxHeight`, `TitleTemplate`.
### FluentNavLink
Navigation link with active state tracking.
```razor
<FluentNavLink Href="/page"
Icon="@(Icons.Regular.Size20.Document)"
Match="NavLinkMatch.Prefix"
Target="_blank"
Disabled="false">
Page Title
</FluentNavLink>
```
Parameters: `Href`, `Target`, `Match` (`NavLinkMatch.Prefix` default, or `All`), `ActiveClass`, `Icon`, `IconColor`, `Disabled`, `Tooltip`.
All nav components inherit from `FluentNavBase` which provides: `Icon`, `IconColor`, `CustomColor`, `Disabled`, `Tooltip`.
### FluentBreadcrumb / FluentBreadcrumbItem
```razor
<FluentBreadcrumb>
<FluentBreadcrumbItem Href="/">Home</FluentBreadcrumbItem>
<FluentBreadcrumbItem Href="/products">Products</FluentBreadcrumbItem>
<FluentBreadcrumbItem>Current Page</FluentBreadcrumbItem>
</FluentBreadcrumb>
```
### FluentTab / FluentTabs
```razor
<FluentTabs @bind-ActiveTabId="@activeTab">
<FluentTab Id="tab1" Label="Details">
Details content
</FluentTab>
<FluentTab Id="tab2" Label="History">
History content
</FluentTab>
</FluentTabs>
```

View File

@@ -0,0 +1,129 @@
# Setup and Configuration
## NuGet Packages
| Package | Purpose |
|---|---|
| `Microsoft.FluentUI.AspNetCore.Components` | Core component library (required) |
| `Microsoft.FluentUI.AspNetCore.Components.Icons` | Icon package (optional, recommended) |
| `Microsoft.FluentUI.AspNetCore.Components.Emojis` | Emoji package (optional) |
| `Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter` | EF Core adapter for DataGrid (optional) |
| `Microsoft.FluentUI.AspNetCore.Components.DataGrid.ODataAdapter` | OData adapter for DataGrid (optional) |
## Program.cs Registration
```csharp
builder.Services.AddFluentUIComponents();
```
### Configuration Options (LibraryConfiguration)
| Property | Type | Default | Notes |
|---|---|---|---|
| `UseTooltipServiceProvider` | `bool` | `true` | Registers `ITooltipService`. If true, you MUST add `<FluentTooltipProvider>` to layout |
| `RequiredLabel` | `MarkupString` | Red `*` | Custom markup for required field indicators |
| `HideTooltipOnCursorLeave` | `bool` | `false` | Close tooltip when cursor leaves both anchor and tooltip |
| `ServiceLifetime` | `ServiceLifetime` | `Scoped` | Only `Scoped` or `Singleton`. `Transient` throws! |
| `ValidateClassNames` | `bool` | `true` | Validates CSS class names against `^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$` |
| `CollocatedJavaScriptQueryString` | `Func<string, string>?` | `v={version}` | Cache-busting for JS files |
### ServiceLifetime by hosting model
| Hosting model | ServiceLifetime |
|---|---|
| Blazor Server | `Scoped` (default) |
| Blazor WebAssembly Standalone | `Singleton` |
| Blazor Web App (Interactive) | `Scoped` (default) |
| Blazor Hybrid (MAUI) | `Singleton` |
## MainLayout.razor Template
```razor
@inherits LayoutComponentBase
<FluentLayout>
<FluentHeader Height="50">
My App
</FluentHeader>
<FluentStack Orientation="Orientation.Horizontal" HorizontalGap="0" Style="height: 100%;">
<FluentNavMenu Width="250" Collapsible="true" Title="Navigation">
<FluentNavLink Href="/" Icon="@(Icons.Regular.Size20.Home)" Match="NavLinkMatch.All">Home</FluentNavLink>
<FluentNavLink Href="/counter" Icon="@(Icons.Regular.Size20.NumberSymbol)">Counter</FluentNavLink>
<FluentNavGroup Title="Settings" Icon="@(Icons.Regular.Size20.Settings)">
<FluentNavLink Href="/settings/general">General</FluentNavLink>
<FluentNavLink Href="/settings/profile">Profile</FluentNavLink>
</FluentNavGroup>
</FluentNavMenu>
<FluentBodyContent>
<FluentStack Orientation="Orientation.Vertical" Style="padding: 1rem;">
@Body
</FluentStack>
</FluentBodyContent>
</FluentStack>
</FluentLayout>
@* Required providers — place after FluentLayout *@
<FluentToastProvider />
<FluentDialogProvider />
<FluentMessageBarProvider />
<FluentTooltipProvider />
<FluentKeyCodeProvider />
@* Theme — place at root *@
<FluentDesignTheme Mode="DesignThemeModes.System"
OfficeColor="OfficeColor.Teams"
StorageName="mytheme" />
```
Or use the convenience component:
```razor
<FluentMainLayout Header="@header"
NavMenuContent="@navMenu"
Body="@body"
HeaderHeight="50"
NavMenuWidth="250"
NavMenuTitle="Navigation" />
@code {
private RenderFragment header = @<span>My App</span>;
private RenderFragment navMenu = @<div>
<FluentNavLink Href="/">Home</FluentNavLink>
</div>;
private RenderFragment body = @<div>@Body</div>;
}
```
## _Imports.razor
Add this to your `_Imports.razor`:
```razor
@using Microsoft.FluentUI.AspNetCore.Components
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
```
## Static Web Assets
No manual `<link>` or `<script>` tags are needed. The library uses:
- **CSS**: `reboot.css` (normalization) + component-scoped CSS — auto-loaded via static web assets
- **JS**: `lib.module.js` — auto-loaded via Blazor's JS initializer system
- Component-specific JS (e.g. DataGrid, Autocomplete) — lazy-loaded on demand
All served from `_content/Microsoft.FluentUI.AspNetCore.Components/`.
## Services Registered
Services automatically registered by `AddFluentUIComponents()`:
| Service | Implementation | Purpose |
|---|---|---|
| `GlobalState` | `GlobalState` | Shared application state |
| `IToastService` | `ToastService` | Toast notifications (needs `FluentToastProvider`) |
| `IDialogService` | `DialogService` | Dialogs and panels (needs `FluentDialogProvider`) |
| `IMessageService` | `MessageService` | Message bars (needs `FluentMessageBarProvider`) |
| `IKeyCodeService` | `KeyCodeService` | Keyboard shortcuts (needs `FluentKeyCodeProvider`) |
| `IMenuService` | `MenuService` | Context menus |
| `ITooltipService` | `TooltipService` | Tooltips (needs `FluentTooltipProvider`, opt-in via `UseTooltipServiceProvider`) |

View File

@@ -0,0 +1,103 @@
# Theming
## FluentDesignTheme (recommended)
The primary theming component. Place it at the root of your app.
```razor
<FluentDesignTheme Mode="DesignThemeModes.System"
OfficeColor="OfficeColor.Teams"
StorageName="mytheme" />
```
### Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `Mode` | `DesignThemeModes` | `System` | `Light`, `Dark`, or `System` (follows OS) |
| `CustomColor` | `string?` | null | Hex accent color (e.g. `"#0078D4"`) |
| `OfficeColor` | `OfficeColor?` | null | Preset accent: `Teams`, `Word`, `Excel`, `PowerPoint`, `Outlook`, `OneNote` |
| `NeutralBaseColor` | `string?` | null | Neutral palette base hex color |
| `StorageName` | `string?` | null | Persist theme to localStorage under this key |
| `Direction` | `LocalizationDirection?` | null | `Ltr` or `Rtl` |
| `OnLuminanceChanged` | `EventCallback<LuminanceChangedEventArgs>` | | Fired when dark/light mode changes |
| `OnLoaded` | `EventCallback<LoadedEventArgs>` | | Fired when theme is loaded from storage |
### Two-way binding
```razor
<FluentDesignTheme @bind-Mode="@themeMode"
@bind-OfficeColor="@officeColor"
@bind-CustomColor="@customColor"
StorageName="mytheme" />
<FluentSelect Items="@(Enum.GetValues<DesignThemeModes>())"
@bind-SelectedOption="@themeMode"
OptionText="@(m => m.ToString())" />
@code {
private DesignThemeModes themeMode = DesignThemeModes.System;
private OfficeColor? officeColor = OfficeColor.Teams;
private string? customColor;
}
```
### Important: JS interop dependency
`FluentDesignTheme` uses JavaScript interop internally. It will NOT work during server-side pre-rendering. If you need to react to theme changes:
```csharp
// Use OnAfterRenderAsync, NOT OnInitialized
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Safe to interact with design tokens here
}
}
```
## FluentDesignSystemProvider (advanced)
For scoping design tokens to a subtree of the component tree. Provides 50+ CSS custom properties.
```razor
<FluentDesignSystemProvider AccentBaseColor="#0078D4"
NeutralBaseColor="#808080"
BaseLayerLuminance="0.95">
<FluentButton Appearance="Appearance.Accent">Themed Button</FluentButton>
</FluentDesignSystemProvider>
```
## Design Token Classes (DI-based, advanced)
For programmatic token control via dependency injection. Each token is a generated service.
```csharp
@inject AccentBaseColor AccentBaseColor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Set token for a specific element
await AccentBaseColor.SetValueFor(myElement, "#FF0000".ToSwatch());
// Read token value
var currentColor = await AccentBaseColor.GetValueFor(myElement);
// Remove override
await AccentBaseColor.DeleteValueFor(myElement);
}
}
```
## Available DesignThemeModes
- `DesignThemeModes.Light` — light theme
- `DesignThemeModes.Dark` — dark theme
- `DesignThemeModes.System` — follows OS preference
## Available OfficeColor presets
`Teams`, `Word`, `Excel`, `PowerPoint`, `Outlook`, `OneNote`, `Loop`, `Planner`, `SharePoint`, `Stream`, `Sway`, `Viva`, `VivaEngage`, `VivaInsights`, `VivaLearning`, `VivaTopics`.