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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ Then build with Radzen components and the `ag-*` helper classes, and use the she
- **`ag-*` helper classes** — cards, buttons, badges, forms, grids, empty states,
the app shell, and the login card.
- **Components** — `AppShell`, `AppPageTitle`, `LoginShell`, `AppIcon`,
`ThemeToggle`, `LanguageSwitcher`, `PageHeader`, `StatusBadge`, `EmptyState`,
`RowActions`, `IconButton`, `DataCard`, `GridToolbar`, `FilterBar`, `CardForm`,
`FormField`, `SettingToggleRow`, `StatTile`, `LinkButton`, `InfoBox`, and the
cascading `BreadcrumbState`.
`ThemeToggle`, `LanguageSwitcher`, `PageHeader`, `DialogHeader`, `StatusBadge`,
`EmptyState`, `RowActions`, `IconButton`, `DataCard`, `GridToolbar`, `FilterBar`,
`CardForm`, `FormField`, `SettingToggleRow`, `StatTile`, `LinkButton`, `InfoBox`,
and the cascading `BreadcrumbState`.
- **Services & options** — `ConfirmService` (standardised confirm/delete dialogs)
and `DesignBlazorOptions` (e.g. the `BrandName` `AppPageTitle` appends to document
titles), registered via `services.AddDesignBlazor(o => o.BrandName = "Acme")`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<PropertyGroup>
<PackageId>AndreGoepel.Design.Blazor</PackageId>
<Version>1.2.1</Version>
<Version>1.3.0</Version>
<Description>App-neutral design system for Blazor: emerald-accented light/dark tokens, a Radzen reskin, self-hosted fonts, EN/DE localization, and shell / login building blocks.</Description>
<PackageTags>blazor;design-system;radzen;theme;ui</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
23 changes: 23 additions & 0 deletions src/AndreGoepel.Design.Blazor/Components/DialogHeader.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@* Rich dialog title (DESIGN.md §5 "Dialogs"): a title + optional subtitle, with a
bottom border separating it from the dialog body — pass this as the
`titleContent` RenderFragment to `DialogService.OpenAsync` instead of a plain
string when the dialog needs more than a bare title. Radzen still renders its
own close button around whatever `titleContent` returns. *@

<div class="ag-dialog-header">
<RadzenText TextStyle="TextStyle.H6" TagName="TagName.H2">@Title</RadzenText>
@if (!string.IsNullOrWhiteSpace(Subtitle))
{
<RadzenText TextStyle="TextStyle.Body2" Style="color: var(--rz-text-secondary-color);">@Subtitle</RadzenText>
}
</div>

@code {
/// <summary>The dialog title.</summary>
[Parameter, EditorRequired]
public string Title { get; set; } = "";

/// <summary>Optional short description shown under the title.</summary>
[Parameter]
public string? Subtitle { get; set; }
}
13 changes: 12 additions & 1 deletion src/AndreGoepel.Design.Blazor/Components/FormField.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
label doesn't match the design. *@

<RadzenStack Gap="0.4rem">
<RadzenLabel Text="@Label" Component="@For" />
<RadzenLabel Component="@For">
@Label
@if (Required)
{
<span class="ag-required" aria-hidden="true">*</span>
}
</RadzenLabel>
@ChildContent
@if (!string.IsNullOrWhiteSpace(Hint))
{
Expand All @@ -30,4 +36,9 @@
/// <summary>Optional muted help text shown under the control.</summary>
[Parameter]
public string? Hint { get; set; }

/// <summary>Whether to show a red asterisk after the label. Purely visual — pair
/// it with a <c>RadzenRequiredValidator</c> on the control for actual validation.</summary>
[Parameter]
public bool Required { get; set; }
}
29 changes: 27 additions & 2 deletions src/AndreGoepel.Design.Blazor/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ label doesn't match the design. Put the control + any validators in each

```razor
<CardForm TItem="InputModel" Data="Input" Submit="SaveAsync" Cancel="Cancel" IsBusy="_saving">
<FormField Label="Email" For="Email" Hint="We'll only use this to contact you.">
<FormField Label="Email" For="Email" Required="true" Hint="We'll only use this to contact you.">
<RadzenTextBox @bind-Value="Input.Email" Name="Email" Placeholder="name@example.com" Style="width:100%" />
<RadzenRequiredValidator Component="Email" Text="Email is required" />
<RadzenEmailValidator Component="Email" Text="That is not a valid email" />
Expand All @@ -158,7 +158,9 @@ label doesn't match the design. Put the control + any validators in each

`Submit` fires only when validation passes; while `IsBusy` is true the primary
button shows `BusyText` and both buttons disable. `Cancel` is optional — omit it and
no cancel button is shown.
no cancel button is shown. `FormField`'s `Required` is purely the visual red
asterisk — it doesn't validate anything, so pair it with a
`RadzenRequiredValidator` as above.

Two fields side by side (collapses to one column on narrow screens): wrap them in
`<div class="ag-form-grid">…</div>` inside the `CardForm`.
Expand Down Expand Up @@ -437,6 +439,27 @@ private void OpenRowMenu(MouseEventArgs args, Role role)

The host layout must render `<RadzenContextMenu />` once (the Aspire sample does).

### Dialogs

`DialogService.OpenAsync(string title, ...)` only takes a plain title string —
fine for a one-liner, but a form dialog that needs a subtitle (and a border
separating the header from the body) should pass `DialogHeader` as the
`titleContent` RenderFragment instead. Radzen still renders its own close button
and wrapper around whatever `titleContent` returns:

```razor
var result = await Dialogs.OpenAsync<TItem>(
titleContent: _ =>
@<DialogHeader Title="New customer" Subtitle="Add a customer to the studio's portal." />,
childContent: _ => @<NewCustomerForm />
);
```

Inside the dialog body, use `CardForm`/`FormField` as usual — a `FormField`
that's required visually should set `Required="true"` for the red asterisk
(pair it with a `RadzenRequiredValidator` for the actual validation), and pair
naturally-related fields (e.g. email + mobile) in an `ag-form-grid` as normal.

### Confirmations

Don't call `DialogService.Confirm` directly — inject `ConfirmService` and use it,
Expand Down Expand Up @@ -483,9 +506,11 @@ A page just provides the heading block + form + a centred footer link:
| Class | Purpose |
|---|---|
| `ag-page-head` | header row: heading left, action right (rendered by `PageHeader`) |
| `ag-dialog-header` | bordered dialog title + optional subtitle (rendered by `DialogHeader`) |
| `ag-card-actions` | bordered card footer, right-aligned (add `ag-start` for left; rendered by `CardForm`) |
| `ag-actions-inline` | inline button group that doesn't stretch |
| `ag-form-grid` | two-column field grid (collapses ≤640px) |
| `ag-required` | red asterisk after a required field's label (rendered by `FormField`) |
| `ag-toggle-row`, `ag-toggle-row-text`, `ag-toggle-row-label`, `ag-toggle-row-description` | settings toggle row: label + description + switch (rendered by `SettingToggleRow`) |
| `ag-badge` + `ag-badge-success` / `-danger` / `-warn` / `-info` / `-neutral` | status pills (rendered by `StatusBadge`) |
| `ag-grid-toolbar`, `ag-search`, `ag-search-icon`, `ag-search-input`, `ag-grid-count` | in-card grid toolbar: filter box + row count (rendered by `GridToolbar`, inside `DataCard`) |
Expand Down
17 changes: 17 additions & 0 deletions src/AndreGoepel.Design.Blazor/wwwroot/css/design.css
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,12 @@ h4:focus-visible, h5:focus-visible, h6:focus-visible {
}
}

/* Required-field marker: a small red asterisk after a FormField's label. */
.ag-required {
color: var(--ag-danger);
margin-left: 2px;
}

/* A settings toggle row: label + description on the left, a switch on the
right. Stack them in a card; the last row should drop its border (add
.ag-toggle-row-last, or let SettingToggleRow's own :last-child rule
Expand Down Expand Up @@ -737,6 +743,17 @@ a:hover, .rz-link:hover {
justify-content: flex-start;
}

/* Rich dialog title (title + optional subtitle), rendered inside Radzen's own
title-bar chrome via `DialogHeader` — a bottom border separates it from the
dialog body, matching a card's ag-card-actions footer border. */
.ag-dialog-header {
display: flex;
flex-direction: column;
gap: 4px;
padding-bottom: 14px;
border-bottom: 1px solid var(--ag-border);
}

/* ===================================================== APP SHELL (host) === */
.ag-shell {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using AndreGoepel.Design.Blazor.Components;
using Bunit;

namespace AndreGoepel.Design.Blazor.Tests.Components;

public class DialogHeaderTests : BunitContext
{
[Fact]
public void Render_TitleOnly_ShowsHeadingAndNoSubtitle()
{
// Act
var cut = Render<DialogHeader>(parameters => parameters.Add(p => p.Title, "New customer"));

// Assert
Assert.Equal("New customer", cut.Find("h2").TextContent);
Assert.Empty(cut.FindAll("p.rz-text-body2"));
}

[Fact]
public void Render_WithSubtitle_ShowsSubtitleText()
{
// Act
var cut = Render<DialogHeader>(parameters =>
parameters
.Add(p => p.Title, "New customer")
.Add(p => p.Subtitle, "Add a customer to the studio's portal.")
);

// Assert
Assert.Contains("Add a customer to the studio's portal.", cut.Markup);
}

[Fact]
public void Render_WithoutSubtitle_OmitsSubtitleParagraph()
{
// Act
var cut = Render<DialogHeader>(parameters => parameters.Add(p => p.Title, "New customer"));

// Assert
Assert.DoesNotContain("rz-text-body2", cut.Markup);
}

[Fact]
public void Render_UsesAgDialogHeaderClass()
{
// Act
var cut = Render<DialogHeader>(parameters => parameters.Add(p => p.Title, "New customer"));

// Assert
Assert.NotNull(cut.Find(".ag-dialog-header"));
}
}
23 changes: 23 additions & 0 deletions tests/AndreGoepel.Design.Blazor.Tests/Components/FormFieldTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,27 @@ public void Render_WithoutHint_OmitsCaption()
// Assert
Assert.Empty(cut.FindAll(".rz-text-caption"));
}

[Fact]
public void Render_Required_ShowsAsteriskAfterLabel()
{
// Act
var cut = Render<FormField>(parameters =>
parameters.Add(p => p.Label, "Email").Add(p => p.Required, true)
);

// Assert
Assert.Contains("Email", cut.Find("label").TextContent);
Assert.NotNull(cut.Find("label .ag-required"));
}

[Fact]
public void Render_NotRequired_OmitsAsterisk()
{
// Act
var cut = Render<FormField>(parameters => parameters.Add(p => p.Label, "Email"));

// Assert
Assert.Empty(cut.FindAll(".ag-required"));
}
}