8.5 KiB
Shared library: ZB.MOM.WW.Theme
Status: Built (0.1.0). The RCL lives at
scadaproj/ZB.MOM.WW.Theme/ — built and tested. Adoption
by the three apps is follow-on, tracked in ../GAPS.md. Realizes
../spec/SPEC.md.
Package
One NuGet package — unlike ZB.MOM.WW.Auth's four-package split, there are no
tokens-only or components-only consumers; all three apps consume the full kit.
| Package | Target | Notes |
|---|---|---|
ZB.MOM.WW.Theme |
net10.0 Razor Class Library |
Tokens + fonts + layout CSS + all components |
Published to the Gitea NuGet feed; Version 0.1.0. SemVer — token changes are
breaking (major bump). Build from scadaproj/ZB.MOM.WW.Theme/:
dotnet build -c Release # 0 warnings (TreatWarningsAsErrors)
dotnet test # 32 bUnit tests
./build/pack.sh # → ./artifacts/ZB.MOM.WW.Theme.0.1.0.nupkg
Consumer matrix
All three apps consume the single RCL. No optional packages.
| Consumer | Surface | Consumes |
|---|---|---|
OtOpcUa ZB.MOM.WW.OtOpcUa.AdminUI |
Admin UI (Blazor SSR, side rail) | ZB.MOM.WW.Theme |
MxAccessGateway ZB.MOM.WW.MxGateway.Server |
Dashboard (Blazor SSR) | ZB.MOM.WW.Theme |
ScadaBridge ZB.MOM.WW.ScadaBridge.Host + ZB.MOM.WW.ScadaBridge.CentralUI |
Central UI (Blazor SSR) | ZB.MOM.WW.Theme |
Static assets
Served at _content/ZB.MOM.WW.Theme/… by ASP.NET's static-web-asset pipeline.
| Path | Contents |
|---|---|
css/theme.css |
Design tokens, typography, Bootstrap 5 overrides (379 lines) |
css/layout.css |
Side-rail shell layout, collapsible nav CSS, StatusPill variants, TechCard/TechField helpers |
fonts/ibm-plex-sans-400.woff2 |
IBM Plex Sans Regular — vendored, no CDN |
fonts/ibm-plex-sans-600.woff2 |
IBM Plex Sans SemiBold — vendored, no CDN |
fonts/ibm-plex-mono-500.woff2 |
IBM Plex Mono Medium — vendored, no CDN |
theme.css uses url('../fonts/ibm-plex-*.woff2') — the correct relative path from
css/ to fonts/ in the static-web-asset tree.
Component API
Namespace: ZB.MOM.WW.Theme. All components live in this flat namespace; one
@using ZB.MOM.WW.Theme in _Imports.razor covers everything.
ThemeHead
Emits <link> tags for theme.css and layout.css. No parameters.
<ThemeHead />
Place in App.razor <head> after the app's Bootstrap link.
ThemeShell
Canonical side-rail chassis. Not a LayoutComponentBase — delegated to from the app's
thin MainLayout. The Accent parameter overrides --accent for the shell subtree.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
Product |
string |
Yes | — | Product name rendered in BrandBar |
Accent |
string? |
No | null |
Override --accent for this app (e.g. #2f855a) |
Logo |
RenderFragment? |
No | null |
Custom logo; replaces default ▐ glyph |
Nav |
RenderFragment? |
No | null |
Rail nav items (NavRailSection / NavRailItem) |
RailFooter |
RenderFragment? |
No | null |
Session block / sign-out at rail bottom |
ChildContent |
RenderFragment? |
No | null |
Page body (@Body from MainLayout) |
Adoption pattern — the thin MainLayout:
@* Components/Layout/MainLayout.razor — replaces the app's existing MainLayout *@
@inherits LayoutComponentBase
<ThemeShell Product="OtOpcUa" Accent="#2f5fd0">
<Nav>
<NavRailSection Title="Navigation">
<NavRailItem Href="/" Text="Overview" Match="NavLinkMatch.All" />
<NavRailItem Href="/clusters" Text="Clusters" />
</NavRailSection>
</Nav>
<RailFooter>
@* AuthorizeView session block / sign-out link *@
</RailFooter>
<ChildContent>@Body</ChildContent>
</ThemeShell>
BrandBar
Brand glyph + product name. Rendered inside ThemeShell's rail header; also usable
standalone.
| Parameter | Type | Required | Notes |
|---|---|---|---|
Product |
string |
Yes | Displayed product name |
Logo |
RenderFragment? |
No | Replaces default ▐ glyph when provided |
NavRailItem
One rail navigation link. Wraps Blazor <NavLink class="rail-link">.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
Href |
string |
Yes | — | Link target |
Text |
string |
Yes | — | Label text |
Icon |
RenderFragment? |
No | null |
Optional icon span |
Match |
NavLinkMatch |
No | Prefix |
Active-class matching behavior |
NavRailSection
Collapsible nav section group using CSS-only <details open> — no JavaScript, works in
static Blazor SSR. Apps that need interactive cookie-persisted expand state may keep a
bespoke interactive NavSection alongside this.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
Title |
string |
Yes | — | Eyebrow label |
Expanded |
bool |
No | true |
Initial open state |
ChildContent |
RenderFragment? |
No | null |
NavRailItem children |
StatusPill
Inline status chip. Maps StatusState to a token-based chip class.
| Parameter | Type | Required | Notes |
|---|---|---|---|
State |
StatusState |
Yes | Ok, Warn, Bad, Idle, Info |
ChildContent |
RenderFragment? |
No | Label text |
public enum StatusState { Ok, Warn, Bad, Idle, Info }
CSS classes emitted: chip chip-ok / chip-warn / chip-bad / chip-idle / chip-info.
LoginCard
Static form-POST sign-in card. Login must use a static form POST — SignInAsync must
run before the HTTP response starts; an interactive EventCallback fires too late.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
Product |
string |
Yes | — | Product name in the card heading |
Action |
string |
No | /auth/login |
Form action attribute |
ReturnUrl |
string? |
No | null |
Rendered as <input type="hidden" name="returnUrl"> |
Error |
string? |
No | null |
Displayed as an error notice above the submit button |
ChildContent |
RenderFragment? |
No | null |
For <AntiforgeryToken/> |
Required: inject <AntiforgeryToken/> via ChildContent and validate ReturnUrl
server-side before redirecting (open-redirect risk).
<LoginCard Product="OtOpcUa" Action="/auth/login" ReturnUrl="@safeUrl" Error="@errorMsg">
<AntiforgeryToken />
</LoginCard>
TechButton
Themed button wrapping Bootstrap .btn classes.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
Variant |
ButtonVariant |
No | Primary |
Primary, Secondary, Danger, Ghost |
Type |
string |
No | "button" |
HTML type attribute |
Busy |
bool |
No | false |
Disables button + shows spinner |
ChildContent |
RenderFragment? |
No | null |
Button label |
| (splatted) | IDictionary<string,object>? |
No | — | Passes through arbitrary HTML attributes |
public enum ButtonVariant { Primary, Secondary, Danger, Ghost }
TechCard
Panel with optional header, body, and footer slots.
| Parameter | Type | Required | Notes |
|---|---|---|---|
Title |
string? |
No | String title for the panel header (alternative to Header slot) |
Header |
RenderFragment? |
No | Custom panel header (takes precedence over Title) |
ChildContent |
RenderFragment? |
No | Panel body content |
Footer |
RenderFragment? |
No | Panel footer (padded, top-bordered) |
Class |
string? |
No | Additional CSS classes on the root <section class="panel"> |
TechField
Labeled form-field wrapper: label, input slot, hint text, and inline error.
| Parameter | Type | Required | Notes |
|---|---|---|---|
Label |
string |
Yes | <label> text |
Hint |
string? |
No | Rendered as .form-text below the input |
Error |
string? |
No | Rendered as .field-error.s-bad below the input |
ChildContent |
RenderFragment? |
No | The <input>, <select>, or other control |
Notes
- Bootstrap is not vendored. Each app keeps its own Bootstrap
<link>. The RCL'stheme.cssoverrides--bs-*tokens to align Bootstrap with Technical-Light but does not ship Bootstrap itself. - No global JavaScript.
NavRailSectionis CSS-only (<details>). Apps may add their ownnav-state.jsfor interactive expand-state if needed (OtOpcUa has one). - No auth logic. The RCL is UI-only. Wire
LoginCardtoZB.MOM.WW.Authendpoints in the app. - No data grids, modals, or domain-specific components. These stay per-project.