Files
scadaproj/ZB.MOM.WW.Theme
Joseph Doherty e7a126a9fc feat(theme): upstream the Bootstrap .btn sizing block into the kit (0.4.0)
Bootstrap hard-codes --bs-btn-font-size: 1rem, oversized against the kit's
0.9rem body. All five consuming apps had independently written the identical
block into their own stylesheets (ignitionoee app.css, OtOpcUa site.css, mxgw
site.css, HistorianGateway site.css, ScadaBridge site.css), each carrying a
'delete this when the kit ships a .btn rule' comment. This is that release.

Sized by CSS-variable override only, never by redeclaring box properties:
Bootstrap's .btn-group seam/radius machinery reads the same variables, so a
literal font-size at equal specificity flattens .btn-sm into a padding-only
difference. That was a real defect in mxgw's local copy, so the rationale is
recorded in the CSS to stop it being 'simplified' back.

Lands in layout.css, which ThemeHead emits after theme.css and both after
Bootstrap, so the override wins on source order at equal specificity.

Ships in the same 0.4.0 release as rail-btn-block. Consumers get the sizing on
bump (not opt-in, unlike rail-btn-block) and should delete their local copies
in the same change. Build 0 errors, 48/48 tests, packed asset verified to carry
.btn, .btn-sm/.btn-group-sm and rail-btn-block with no literal font-size.
2026-08-11 02:17:16 -04:00
..

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. All five consuming apps had written this same block into their own stylesheets; delete the local copy when you bump to 0.4.0 (each carries a comment saying so). 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