Files
scadaproj/ZB.MOM.WW.Theme

ZB.MOM.WW.Theme

Shared Technical-Light UI kit for the ZB.MOM.WW SCADA family: design tokens + IBM Plex fonts (static web assets) and a canonical side-rail shell + widgets. The kit ships one .NET 10 Razor Class Library (RCL) with CSS custom-property tokens, the three IBM Plex woff2 fonts, and side-rail layout CSS — all served from _content/ZB.MOM.WW.Theme/… — plus a set of Blazor SSR components that carry no inline colours and reuse the token classes. Bootstrap 5 is not vendored; each app keeps its own Bootstrap link.

The kit ships no JavaScript: every interactive affordance — the narrow-viewport side-rail hamburger and the collapsible nav sections — is a native CSS-only <details>/<summary> disclosure, so the chrome works unchanged in static Blazor SSR with only Bootstrap CSS present (no Bootstrap collapse JS bundle required).

Adopt

  1. Reference the NuGet package in your app; keep your own Bootstrap 5 <link> in App.razor.

  2. In App.razor <head>, after your Bootstrap link, add <ThemeHead />:

    <link rel="stylesheet" href="..." />  @* Bootstrap — yours, not vendored by the kit *@
    <ThemeHead />
    
  3. Make your MainLayout a thin delegate to ThemeShell — @layout cannot pass parameters, so ThemeShell is a regular component and MainLayout is a 3-line wrapper:

    @* Layouts/MainLayout.razor *@
    @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>@* session info / sign-out link *@</RailFooter>
        <ChildContent>@Body</ChildContent>
    </ThemeShell>
    

Add @using ZB.MOM.WW.Theme to your _Imports.razor so all components are available without per-file usings.

Login page

Use <LoginCard> for the sign-in form. The card posts to a server endpoint (/auth/login by default) so SignInAsync can run before the response starts. You must inject <AntiforgeryToken/> inside the card and validate ReturnUrl server-side before redirecting (open-redirect risk):

<LoginCard Product="OtOpcUa" Action="/auth/login" ReturnUrl="@safeReturnUrl" Error="@errorMsg">
    <AntiforgeryToken />
</LoginCard>

Components

Component Parameters (key) Notes
ThemeHead — Emits <link> tags for theme.css and layout.css. Place in <head> after Bootstrap.
ThemeShell Product*, Accent, Logo, Nav, RailFooter, ChildContent Side-rail chassis. Not a layout — delegated to from MainLayout. Accent overrides --accent token.
BrandBar Product*, Logo Brand glyph + product name; rendered inside ThemeShell's rail header.
NavRailItem Href, Text, Icon, Match Wraps <NavLink class="rail-link">.
NavRailSection Title*, Expanded (default true), ChildContent CSS-only collapsible <details> group; no JS, works in static SSR.
StatusPill (StatusState) State* (Ok/Warn/Bad/Idle/Info), ChildContent Inline chip. StatusState enum is in ZB.MOM.WW.Theme.
LoginCard Product*, Action (default /auth/login), ReturnUrl, Error, ChildContent Static form-POST sign-in card. Inject <AntiforgeryToken/> via ChildContent; validate ReturnUrl server-side.
TechButton (ButtonVariant) Variant (Primary/Secondary/Danger/Ghost), Type, Busy, ChildContent, splatted attrs Busy disables the button and shows a spinner. ButtonVariant enum is in ZB.MOM.WW.Theme.
TechCard Title, Header, ChildContent, Footer, Class Panel with optional head/body/footer slots.
TechField Label*, Hint, Error, ChildContent Form field wrapper with label, hint text, and inline error.

* EditorRequired parameter.

Flat namespace: all components and enums live in ZB.MOM.WW.Theme. One @using covers everything.

Static assets

Served at _content/ZB.MOM.WW.Theme/… by the ASP.NET static-web-asset pipeline:

Path Contents
css/theme.css Design tokens (--accent, --ok, --warn, …), typography, utility helpers
css/layout.css Side-rail shell layout, collapsible nav, StatusPill variants, card/field helpers, Bootstrap .btn sizing

Bootstrap button sizing (new in 0.4.0). The kit now normalizes .btn / .btn-sm against its 0.9rem body via --bs-btn-* variable overrides, because Bootstrap hard-codes --bs-btn-font-size: 1rem. Several consuming apps had written this same block into their own stylesheets; trim the local copy when you bump to 0.4.0 — trim rather than delete, because a local .btn may also carry app-specific shape declarations the kit does not ship.

This is not universal, and checking it takes two greps, not one. An earlier version of this note claimed all five consumers carried the block. They do not — and the follow-up correction, that the missing-block app would therefore be restyled by the bump, was also wrong.

Question Check
Does the app declare its own .btn sizing? grep bs-btn-font-size, excluding lib/bootstrap
Does the app use Bootstrap buttons at all? count class="…" values carrying a bare btn token

HistorianGateway answers no to both — all 15 of its buttons use the kit's own .rail-btn chassis class — so the kit's .btn rule matches nothing there and bumping it changes nothing visible. The first question alone is misleading in both directions: "no local block" does not imply "the kit now supplies sizing the app was missing." Size buttons by overriding the --bs-btn-* variables, never by redeclaring box properties — the .btn-group seam and radius machinery reads those variables, so a literal font-size at equal specificity silently reduces .btn-sm to a padding-only difference.

Rail-footer actions: class="rail-btn" is the inline default; add rail-btn-block (class="rail-btn rail-btn-block") for a full-width footer button. Use the modifier rather than redeclaring .rail-btn in an app stylesheet — a local override that sets width: 100% overflows the rail, because the width resolves against the containing block and the inline version's 0 0.6rem side margins are then added on top.

| fonts/ibm-plex-sans-400.woff2 | IBM Plex Sans Regular | | fonts/ibm-plex-sans-600.woff2 | IBM Plex Sans SemiBold | | fonts/ibm-plex-mono-500.woff2 | IBM Plex Mono Medium |

theme.css declares @font-face with url('../fonts/…') — correct relative path from css/ to fonts/. (OtOpcUa's original url('fonts/…') was a latent 404; the kit fixes it.)

All colours are CSS custom-property tokens declared once in theme.css's :root block — no hardcoded hex appears outside it. Derived border/hover/wash shades (e.g. --ok-border, --warn-ink, --info-bg, --hover-bg, --active-bg, --zebra-bg) are named tokens too, so a token-block edit re-themes the whole kit.

Build

# from ZB.MOM.WW.Theme/
dotnet build -c Release   # TreatWarningsAsErrors — expect 0 warnings
dotnet test               # 38 bUnit tests
./build/pack.sh           # → ./artifacts/ZB.MOM.WW.Theme.0.1.0.nupkg