Instruction file imported from houranigit/NAGS.OperationSystem (
.cursor/rules/frontend-blazor.mdc). Copyright stays with the author.
Frontend Blazor
- The Blazor portal is the operational product UI: clarity, density, repeatable workflows — not a marketing site.
- Use Radzen.Blazor with the free default theme (
RadzenTheme Theme="default"inApp.razor). Material3 and other premium themes are paid and must not be used. Brand the default theme by mapping Radzen--rz-*variables onto the--os-*tokens inapp.css. Do not add Bootstrap, Tailwind, or another component library. - Design tokens first. Colors, spacing, radius, and typography live in
wwwroot/tokens.cssas CSS variables (--os-*). Never hardcode hex/rgb in.razoror.razor.cssfiles. Layout shell styles inMainLayout.razor.cssmust use tokens too. - Shared shells, not one-off pages. Build pages from components under
OperationsSystem.Blazor.Client/Shared/:PageHeader— title, optional description, actions slotAuthLayout— centered card for login/activationLoadingCard— spinner + messageEmptyState— icon/title/description for no dataDetailField— label + value grid cell (replaces raw.detail-fielddivs)
- Page recipes. Every feature page follows one of these shapes:
- List —
PageHeader+ toolbar (search/filters) +RadzenDataGrid+ pagination - Detail —
PageHeader+RadzenCardsections +DetailFieldgrid - Form —
PageHeader+RadzenTemplateForminsideRadzenCard; primary action right/bottom - Auth —
AuthLayoutonly; noMainLayoutchrome
- List —
- Radzen defaults. Prefer Radzen layout primitives over custom CSS:
RadzenStack/RadzenRow/RadzenColumnfor spacing and responsive gridsRadzenCardfor grouped contentRadzenFormFieldwrapping inputs (never bareRadzenTextBoxwithout a field label)Gap="1rem"orGap="1.5rem"— do not invent random gap valuesButtonStyle.Primaryfor main actions,ButtonStyle.Lightfor secondary,ButtonStyle.Dangerfor destructive
- States are mandatory. Every data view handles loading, empty, and error explicitly using the shared shells — not ad-hoc
@ifblocks per page. - No inline
Style=on Radzen components exceptwidth: 100%on full-width inputs inside forms. Use tokens, component parameters, or scoped CSS classes prefixed withos-. - i18n and RTL. Do not hardcode user-facing strings. Use the localization layer once wired; until then, centralize strings in a static
UiStringsclass — never scatter literals. Support RTL with logical properties (margin-inline,padding-inline,border-inline) and test[dir="rtl"]. - Auth guard pattern. Protected pages use a single wrapper (e.g.
RequireAuth) instead of duplicatingAuth.InitializeAsync+ redirect logic in every panel. - API access. Use
BrowserApiClient/ typed clients — no rawHttpClientin components. - When adding a new page, copy an existing recipe from
Shared/Templates/(or the nearest sibling page) and adapt content — do not invent a new layout pattern.
Example — list page skeleton
<PageHeader Title="@T["users.title"]" Description="@T["users.description"]">
<Actions>
<RadzenButton Text="@T["users.invite"]" ButtonStyle="ButtonStyle.Primary" Click="@OpenInvite" />
</Actions>
</PageHeader>
@if (isLoading) { <LoadingCard Message="@T["common.loading"]" /> }
else if (items.Count == 0) { <EmptyState Title="@T["users.empty"]" /> }
else
{
<RadzenCard>
<RadzenDataGrid Data="@items" TItem="UserRow" AllowPaging="true" PageSize="20">
...
</RadzenDataGrid>
</RadzenCard>
}
Anti-patterns
@* BAD: hardcoded color, inline style, custom layout class *@
<p style="color: #64748b">Welcome</p>
<div class="dashboard-grid">...</div>
@* GOOD: token class + Radzen layout *@
<p class="os-muted">@T["dashboard.welcome"]</p>
<RadzenRow Gap="1rem">...</RadzenRow>