Files
scadaproj/ZB.MOM.WW.Theme/README.md
T

11 KiB

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, ButtonSize) Variant (Primary/Secondary/Danger/Ghost), Size (Default/Small/Large), Class, Type, Busy, ChildContent, splatted attrs Busy disables the button and shows a spinner. Caller classes are composed with required variant/size classes.
TreeTable<TRow> (ITreeRow) Rows, Head, Row, Label, Collapsed, OnToggle Stateless preorder tree table. The caller filters visible rows and owns expansion state.
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, shared page/group headers, badges, row actions, info notices, payload blocks and tree-table styles

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               # 60 tests verified for 0.5.0
./build/pack.sh           # → ./artifacts/ZB.MOM.WW.Theme.0.5.0.nupkg

0.5.0 migration (published 2026-11-04)

The tested package was published to the shared Gitea feed on 2026-11-04. An independent feed download exactly matched the reviewed artifact (SHA256 374511ebaaa15be71024051e896c8e7e59a8951b0d51a990fa48fbe378ac6485). The ignitionoee router deployed this package to CR on 2026-11-04; other consumer rollouts remain separate. It retains the 0.4.2 .rise animation fill-mode fix. The published package is immutable; this source documentation update does not repack or replace it.

The shell now keeps page content visible when mobile navigation is collapsed. The outer disclosure's ::details-content wrapper is dissolved at every width; an explicit mobile closed-state selector hides only the rail. Author display rules retain the page for older browsers without ::details-content support. The menu remains a native keyboard-accessible disclosure without custom JavaScript. Mobile pages are constrained to the shell width so wide tables scroll inside their table wrapper instead of widening the whole document.

The remaining ignitionoee app.css rules now ship in layout.css: page and section headers/meta, group headings/meta, count badges, .notice.info, .row-actions, td.row-actions, and .check-data. Consumers may delete matching local copies after adopting this package. The element-qualified row-action selector must remain: the bare class loses to .data-table td regardless of stylesheet load order.

Use Size="ButtonSize.Small" or Size="ButtonSize.Large" on TechButton. Class="my-action" and an attribute dictionary's class value are composed with the required button classes. A lowercase class supplied in Razor binds to Class as normal Blazor parameter binding. Supplying both spellings as separate component parameters is unnecessary; use Class plus AdditionalAttributes when merging two sources. Explicit Busy and Type continue to control the underlying attributes.

Implement ZB.MOM.WW.Theme.ITreeRow on a consumer's row model (Key, Depth, HasChildren). Feed TreeTable<TRow> visible rows in preorder, with each subtree contiguous after its parent. Head supplies all header cells; Row supplies cells after the component's name cell; Label supplies the name. OnToggle reports the stable row key, and the caller supplies a new Collapsed state and filters Rows. The component never filters or mutates caller state. Indentation supports arbitrary depth through --tt-depth; styles ship once, with no per-instance stylesheet. Leaf controls preserve label alignment and omit subtree ARIA attributes. Interactive expand/collapse requires an interactive Blazor host and Bootstrap Icons for chevrons.

The test harness uses bUnit 2.7.2 and directly pins AngleSharp 1.5.0. This avoids the vulnerable parser minimum (GHSA-pgww-w46g-26qg) and the binary incompatibility between bUnit 1.x and the patched parser. These dependencies belong to the test project; the Theme runtime package gains none.