0.4.0's rail-btn-block used display:block + width:auto, which fills for an <a> but SHRINK-WRAPS a <button> — a form control does not take the fill-available behaviour a non-replaced block box gets. Measured in one 197px rail-foot container: <a> filled to 177.42px (0 short), <button> came up 114px short. That matters because a rail foot normally holds both element types: Sign In is an anchor, Sign Out is a form-submit button. So 0.4.0's modifier got one right and one wrong, and the wrong one is what signed-in operators see — worse than the width:100% overflow it was meant to replace (~10px of visible clipping). Fixed with an explicit calc(100% - 1.2rem) + box-sizing:border-box, which measures 0px short for both element types. Both rejected shorthands and their measured failures are recorded in the CSS so neither is reintroduced as a 'simplification'. Found by HistorianGateway while evaluating adoption; it held off adopting rather than trading one defect for a larger one. No consumer had adopted the modifier, so nothing shipped broken. The .btn half of 0.4.0 is unaffected and is already live-verified in three apps. Build 0 errors, 48/48 tests.
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) |
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