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
-
Reference the NuGet package in your app; keep your own Bootstrap 5
<link>inApp.razor. -
In
App.razor<head>, after your Bootstrap link, add<ThemeHead />:<link rel="stylesheet" href="..." /> @* Bootstrap — yours, not vendored by the kit *@ <ThemeHead /> -
Make your
MainLayouta thin delegate toThemeShell—@layoutcannot pass parameters, soThemeShellis a regular component andMainLayoutis 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.