# 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 `<ThemeShell>` 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 (`<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):

```razor
@* 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:**
1. Add `<PackageReference Include="ZB.MOM.WW.Theme">` in the app.
2. In `App.razor` `<head>`, after Bootstrap: `<ThemeHead />`.
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. `<ThemeHead />` in `App.razor` `<head>` (after Bootstrap); per-app `theme.css` copy and
   IBM Plex `.woff2` files deleted from `wwwroot/`.
3. `MainLayout` replaced with the thin-delegation pattern wrapping `<ThemeShell>`.
4. Nav rebuilt with `NavRailItem` / `NavRailSection`.
5. Local `StatusBadge` / `StatusChip` component deleted; replaced by `<StatusPill>`.
6. Login form replaced with `<LoginCard>` (static form POST preserved; `<AntiforgeryToken/>`
   inside `ChildContent`; `ReturnUrl` validated server-side).
7. Per-app `site.css` page-layout residual kept; scoped `.razor.css` files kept unchanged.
