10 KiB
UI Theme — normalized target spec
Status: Draft. The single design the sister projects converge on. Derived from the
three code-verified current-state docs (../current-state/). Goal is path to shared
code (../shared-contract/ZB.MOM.WW.Theme.md), so each normalized section maps to a
shared library seam.
0. Scope
Normalized here: the "Technical-Light" design token set; IBM Plex typography;
the canonical side-rail layout shell; the component kit (shell, status pill, login card,
common controls); delivery via the ZB.MOM.WW.Theme RCL.
Explicitly NOT normalized (domain-specific — keep per project): each app's site.css
residual page layout, route/page content, and app-specific scoped .razor.css files.
The kit owns the chrome and tokens, not the app's domain screens. Authorization logic
and interactive nav-state persistence (e.g. OtOpcUa's cookie-persisted rail sections)
are also per-project.
1. Design tokens
All color, typography, and structural values are expressed as CSS custom properties
declared on :root in theme.css. Components carry no hardcoded hex values —
everything references these tokens. Bootstrap 5 --bs-* variables are overridden to
align Bootstrap's defaults with the Technical-Light palette.
The one per-app override allowed is --accent on the ThemeShell root element
(passed via the Accent parameter), giving each app a distinct primary color while
sharing all other tokens.
See DESIGN-TOKENS.md for the full enumeration.
Canonical token groups:
- Surface —
--paper,--card - Ink (text) —
--ink,--ink-soft,--ink-faint - Structure —
--rule,--rule-strong - Accent —
--accent,--accent-deep - Status —
--ok,--warn,--bad,--idle(+-bgvariants) - Typography —
--sans(IBM Plex Sans),--mono(IBM Plex Mono) - Bootstrap overrides —
--bs-body-bg,--bs-body-color,--bs-body-font-family,--bs-body-font-size,--bs-primary,--bs-border-color,--bs-emphasis-color
2. Typography
IBM Plex is vendored (three .woff2 files in the RCL's wwwroot/fonts/); no CDN
dependency so air-gapped fleet deployments keep working.
| Font | Weight | File |
|---|---|---|
| IBM Plex Sans | 400 (regular) | ibm-plex-sans-400.woff2 |
| IBM Plex Sans | 600 (semibold) | ibm-plex-sans-600.woff2 |
| IBM Plex Mono | 500 (medium) | ibm-plex-mono-500.woff2 |
The @font-face declarations in theme.css use url('../fonts/ibm-plex-*.woff2')
— the correct relative path from css/theme.css to fonts/. This is the canonical
path; per-app copies that use url('fonts/…') or url('/fonts/…') are incorrect
(OtOpcUa's url('fonts/…') causes a latent 404 silently masked by system-font fallback).
The RCL fixes this permanently for all consumers.
3. Canonical side-rail layout
The one canonical layout is a side rail (not a top nav bar). Layout structure:
┌─────────────────────────────────────────────────┐
│ .app-shell (flex-row on lg+; flex-col on sm) │
│ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ nav.side-rail │ │ main.page │ │
│ │ .brand │ │ (page body / @Body) │ │
│ │ [Nav slot] │ │ │ │
│ │ .rail-foot │ │ │ │
│ └──────────────────┘ └───────────────────────┘ │
└─────────────────────────────────────────────────┘
Rail width / breakpoint: the rail collapses to a hamburger toggle (data-bs-toggle=collapse)
below Bootstrap's lg breakpoint. Above lg, it is always visible.
ThemeShell is a component, not a layout. @layout in Blazor cannot accept
parameters. Each app therefore keeps a thin 3-line MainLayout : LayoutComponentBase
that delegates to <ThemeShell> with its per-app Product, Accent, Nav, and
RailFooter values (see §4 and ../shared-contract/ZB.MOM.WW.Theme.md).
Nav sections within the rail are CSS-only collapsibles (<details open>). Apps that need
interactive expand-state persistence (e.g. OtOpcUa's cookie-persisted nav) may keep a
bespoke interactive NavSection; the RCL's NavRailSection works without JS and is
compatible with static Blazor SSR.
4. Component contract
Namespace: ZB.MOM.WW.Theme. All components are themed via CSS custom properties —
no inline colors. One @using ZB.MOM.WW.Theme covers every component and enum.
Static-asset entry point
| Component | Description |
|---|---|
ThemeHead |
Emits <link> tags for theme.css and layout.css. Drop in <head> after Bootstrap. |
Layout shell
| Component / Type | Key parameters | Notes |
|---|---|---|
ThemeShell |
Product*, Accent, Logo, Nav, RailFooter, ChildContent |
Canonical side-rail chassis. Not a LayoutComponentBase — delegated to from MainLayout. Accent overrides --accent for the shell subtree. |
BrandBar |
Product*, Logo |
Brand glyph + product name; rendered in the rail header inside ThemeShell. |
NavRailItem |
Href, Text, Icon, Match |
Wraps <NavLink class="rail-link">. Active state via Blazor NavLink. |
NavRailSection |
Title*, Expanded (default true), ChildContent |
CSS-only <details> collapsible group; no JS, works in static SSR. |
Thin-MainLayout delegation pattern (required; see §3):
@* Components/Layout/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 *@</RailFooter>
<ChildContent>@Body</ChildContent>
</ThemeShell>
Widgets
| Component / Type | Key parameters | Notes |
|---|---|---|
StatusPill |
State* (StatusState), ChildContent |
Inline chip. StatusState enum: Ok, Warn, Bad, Idle, Info. Maps state → token class (chip-ok, …). |
LoginCard |
Product*, Action (default /auth/login), ReturnUrl, Error, ChildContent |
Static form-POST sign-in card. ChildContent for <AntiforgeryToken/>. Validate ReturnUrl server-side (open-redirect risk). |
TechButton |
Variant (ButtonVariant), Type, Busy, ChildContent, splatted attrs |
ButtonVariant enum: Primary, Secondary, Danger, Ghost. Busy disables + shows spinner. |
TechCard |
Title, Header, ChildContent, Footer, Class |
Panel with optional head/body/footer slots. |
TechField |
Label*, Hint, Error, ChildContent |
Labeled input wrapper with hint text and inline error. |
* EditorRequired parameter.
Deliberately NOT included (YAGNI / stays per-project): data grids, tree views, multi-select dropdowns, modals, toasts, audit components, page-specific layouts.
5. Delivery
The RCL ships as the single NuGet package ZB.MOM.WW.Theme (.NET 10, Version 0.1.0).
Static assets are served at _content/ZB.MOM.WW.Theme/… by ASP.NET's static-web-asset
pipeline:
| Asset path | Contents |
|---|---|
_content/ZB.MOM.WW.Theme/css/theme.css |
Design tokens, typography, utility helpers |
_content/ZB.MOM.WW.Theme/css/layout.css |
Side-rail layout, collapsible nav, StatusPill variants, card/field helpers |
_content/ZB.MOM.WW.Theme/fonts/ibm-plex-sans-400.woff2 |
IBM Plex Sans Regular |
_content/ZB.MOM.WW.Theme/fonts/ibm-plex-sans-600.woff2 |
IBM Plex Sans SemiBold |
_content/ZB.MOM.WW.Theme/fonts/ibm-plex-mono-500.woff2 |
IBM Plex Mono Medium |
Bootstrap 5 is not vendored by the kit — each app keeps its own Bootstrap <link>.
Adoption entry points:
- Add
<PackageReference Include="ZB.MOM.WW.Theme">in the app. - In
App.razor<head>, after Bootstrap:<ThemeHead />. - Replace
MainLayoutwith the thin-delegation pattern (§3/§4). - Add
@using ZB.MOM.WW.Themeto_Imports.razor.
6. Shared vs per-project
Shared (extracted into the RCL):
| What | Where in RCL |
|---|---|
Design tokens (--paper, --ink, --accent, --ok, …) |
wwwroot/css/theme.css |
IBM Plex fonts (three .woff2) |
wwwroot/fonts/ |
| Side-rail shell layout CSS | wwwroot/css/layout.css |
Side-rail shell components (ThemeShell, BrandBar, nav components) |
Components/ |
Status chip (StatusPill, StatusState) |
Components/ |
Login card (LoginCard) |
Components/ |
Common controls (TechButton, TechCard, TechField) |
Components/ |
Per-project (NOT extracted):
| What | Rationale |
|---|---|
site.css page layout residual |
App-specific page structure varies (body padding, two-column layouts, etc.) |
| Page / route components | Domain content — not a UI kit concern |
Scoped .razor.css files |
Component-specific overrides stay with the component they scope |
| Authorization / session UI | Depends on per-project auth model (ZB.MOM.WW.Auth) |
| Interactive nav-state persistence | Bespoke (OtOpcUa uses a cookie; ScadaBridge / MxGateway use JS state) |
7. Acceptance
A project is considered adopted when all of the following hold:
ZB.MOM.WW.ThemeNuGet package referenced;ZB.MOM.WW.Themein_Imports.razor.<ThemeHead />inApp.razor<head>(after Bootstrap); per-apptheme.csscopy and IBM Plex.woff2files deleted fromwwwroot/.MainLayoutreplaced with the thin-delegation pattern wrapping<ThemeShell>.- Nav rebuilt with
NavRailItem/NavRailSection. - Local
StatusBadge/StatusChipcomponent deleted; replaced by<StatusPill>. - Login form replaced with
<LoginCard>(static form POST preserved;<AntiforgeryToken/>insideChildContent;ReturnUrlvalidated server-side). - Per-app
site.csspage-layout residual kept; scoped.razor.cssfiles kept unchanged.