# 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`](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` (+ `-bg` variants) - **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 `` with its per-app `Product`, `Accent`, `Nav`, and `RailFooter` values (see §4 and [`../shared-contract/ZB.MOM.WW.Theme.md`](../shared-contract/ZB.MOM.WW.Theme.md)). Nav sections within the rail are CSS-only collapsibles (`
`). 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 `` tags for `theme.css` and `layout.css`. Drop in `` **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 ``. Active state via Blazor `NavLink`. | | `NavRailSection` | `Title`*, `Expanded` (default `true`), `ChildContent` | CSS-only `
` collapsible group; no JS, works in static SSR. | **Thin-MainLayout delegation pattern** (required; see §3): ```razor @* Components/Layout/MainLayout.razor *@ @inherits LayoutComponentBase @* session info / sign-out *@ @Body ``` ### 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 ``. 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 ``. **Adoption entry points:** 1. Add `` in the app. 2. In `App.razor` ``, after Bootstrap: ``. 3. Replace `MainLayout` with the thin-delegation pattern (§3/§4). 4. Add `@using ZB.MOM.WW.Theme` to `_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: 1. `ZB.MOM.WW.Theme` NuGet package referenced; `ZB.MOM.WW.Theme` in `_Imports.razor`. 2. `` in `App.razor` `` (after Bootstrap); per-app `theme.css` copy and IBM Plex `.woff2` files deleted from `wwwroot/`. 3. `MainLayout` replaced with the thin-delegation pattern wrapping ``. 4. Nav rebuilt with `NavRailItem` / `NavRailSection`. 5. Local `StatusBadge` / `StatusChip` component deleted; replaced by ``. 6. Login form replaced with `` (static form POST preserved; `` inside `ChildContent`; `ReturnUrl` validated server-side). 7. Per-app `site.css` page-layout residual kept; scoped `.razor.css` files kept unchanged.