# 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 `
`/`` 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 `` in `App.razor`. 2. In `App.razor` ``, **after** your Bootstrap link, add ``: ```razor @* Bootstrap — yours, not vendored by the kit *@ ``` 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: ```razor @* Layouts/MainLayout.razor *@ @inherits LayoutComponentBase @* session info / sign-out link *@ @Body ``` Add `@using ZB.MOM.WW.Theme` to your `_Imports.razor` so all components are available without per-file usings. ### Login page Use `` 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 `` inside the card and **validate `ReturnUrl` server-side** before redirecting (open-redirect risk): ```razor ``` ## Components | Component | Parameters (key) | Notes | |---|---|---| | `ThemeHead` | — | Emits `` tags for `theme.css` and `layout.css`. Place in `` 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 ``. | | `NavRailSection` | `Title`*, `Expanded` (default `true`), `ChildContent` | CSS-only collapsible `
` 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 `` 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` (`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 ```bash # 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` 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](https://github.com/advisories/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.