# UI Theme — gaps & adoption backlog

Divergence of each project from [`spec/SPEC.md`](spec/SPEC.md), and the ordered backlog to
reach adoption of the `ZB.MOM.WW.Theme` shared RCL. Status legend: ⛔ gap · 🟡 partial · ✅ matches.

> **Current state (verified 2026-07-18):** the kit has moved on from the `0.2.0` this banner
> describes — the Gitea feed carries `0.2.0`, `0.2.1`, `0.3.0`, `0.3.1`, and **all four apps are
> pinned to `0.3.1`** (HistorianGateway is a fourth consumer this file predates). The `0.3.0`/`0.3.1`
> nav/aria/login fixes are recorded in `themeissues.md`, not here. Library suite: **48 bUnit tests**,
> green 2026-07-18. History below is accurate for the original cutover.
>
> **✅ ADOPTED 2026-06-03 (local-only).** Backlog #2–#4 implemented across all three apps on each repo's
> **`feat/adopt-zb-theme`** branch — full canonical cutover (SPEC §7): `<ThemeHead/>`/`<ThemeScripts/>`,
> thin `MainLayout` → `<ThemeShell>` + `NavRailItem`/`NavRailSection`, per-app `theme.css`/IBM-Plex fonts/
> `nav-state.js` deleted, `<LoginCard>` sign-in, and `StatusPill` (OtOpcUa's dead `StatusBadge` deleted;
> MxGateway's `StatusBadge` redirected to a thin `StatusPill` adapter; inline domain `.chip-*` kept as page
> content per §6). **Library first enhanced to `0.2.0`** — nav-expand persistence promoted INTO the kit
> (`NavRailSection.Key` → `data-nav-key` + a localStorage `nav-state.js` enhancer emitted by a new
> `<ThemeScripts/>`), so all three apps get uniform persistence from one source (OtOpcUa's bespoke
> cookie/JS-interop nav island retired). 0.2.0 published to the Gitea feed; 44 bUnit tests. **MxGateway
> additionally gained a net-new Blazor `<LoginCard>` `/login` page** reusing its existing hardened
> `POST /login` endpoint (antiforgery + `SanitizeReturnUrl` + `SignInAsync` preserved). Every task spec+code
> reviewed (high-risk via serial spec→code; the MxGateway login via an Opus security review), then
> **fast-forward-merged into each repo's local default and PUSHED to origin (gitea) 2026-06-03** (in sync;
> `feat/*` kept locally): OtOpcUa `master`@`11de14d`, ScadaBridge `main`@`58352a6`, MxGateway `main`@`73e54e2`.
> Plan: `docs/plans/2026-06-03-ui-theme-adoption*.md`. The ⛔/🟡 cells below describe the PRE-adoption
> divergence (kept for history).
>
> **Post-adoption CSS prune (2026-06-03, branch `chore/theme-css-prune` per app).** An audit found each app's
> kept `site.css` still carried the old shell CSS the kit now owns — broader than first logged. Pruned:
> **OtOpcUa** shed a near-verbatim copy of the kit's `layout.css` (`.app-shell`/`.side-rail`/`.rail-link`/
> `.rail-foot`/`.login-*`) plus dead `#sidebar-collapse` (kit emits `#theme-rail`) and `.rail-eyebrow-chevron`
> (−167 lines), keeping only app-only `.rail-eyebrow` + `.chip-alert`/`.chip-caution`; **ScadaBridge** shed the
> dead `.sidebar`/`.nav-link`/`.nav-section-toggle` block (−95), keeping `#reconnect-modal`/`.script-editor-modal`;
> **MxGateway** shed the dead `.sidebar` block + orphaned `.dashboard-login`/`.login-card` (−106), keeping
> `.app-bar` (still used by `/denied`) + the `.chip` override. Each verified unreferenced before removal; all
> three build clean (0 warn/0 err). OtOpcUa's copy was the notable one — it *overrode* the kit, not just dead code.
> **Still deferred:** a kit-side `layout.css` `calc(100vh - 3.3rem)` review; and ScadaBridge's `Host` consumes the
> kit only **transitively via `CentralUI`** (no direct `PackageReference`) — builds green, but an implicit dependency.
>
> _Feed note: the same audit re-confirmed `ZB.MOM.WW.Theme 0.2.0` **is** genuinely on the Gitea feed (registration
> `count:1`, package base `versions:["0.2.0"]`, search `totalHits:1`) — the publish was real, not optimism._

---

## Divergence vs spec

### §1 Design tokens — `theme.css`

| Item | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| Tokens identical to canonical | ✅ identical | ✅ identical | ✅ identical |
| File maintained in one place (RCL) | ⛔ own copy | ⛔ own copy | ⛔ own copy |
| Font path `url('../fonts/…')` | ⛔ `url('fonts/…')` — **latent 404** | 🟡 `url('/fonts/…')` — absolute, not portable | ✅ `url('../fonts/…')` — correct |
| IBM Plex fonts in one place | ⛔ own `wwwroot/fonts/` | ⛔ own `wwwroot/fonts/` | ⛔ own `wwwroot/fonts/` |

→ **Gap T1:** All three apps maintain a copy of `theme.css` — the single-source guarantee
  is broken today. Any token change must be applied in four places (three apps + the RCL)
  once the RCL exists.
→ **Gap T2:** OtOpcUa `url('fonts/…')` is a latent 404 masked by system-font fallback.
  Adoption fixes it automatically.
→ **Gap T3:** Each app vendors fonts — 3× duplication. The RCL eliminates it.

### §2 Typography

All three apps reference IBM Plex via the token stacks. No typography divergence — the
token values are identical. Gap is delivery (T3 above).

### §3 Canonical side-rail layout

| Item | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| `.app-shell` root element | ✅ `div.app-shell` | ⛔ `div.d-flex …` (no `.app-shell`) | ⛔ `div.d-flex …` (no `.app-shell`) |
| Rail CSS class | ✅ `.side-rail` | ⛔ `.sidebar` | ⛔ `.sidebar` |
| Nav item CSS class | ✅ `.rail-link` | ⛔ `.nav-link` | ⛔ `.nav-link` |
| Nav item element | ✅ `<a>` (NavLink) | ⛔ `<li><NavLink>` inside `<ul>` | ⛔ `<li><NavLink>` inside `<ul>` |
| Shell component | ⛔ bespoke `MainLayout` + `NavSidebar` | ⛔ combined `MainLayout` (210 lines) | ⛔ `MainLayout` + `NavMenu` |
| Thin-MainLayout pattern | ⛔ not yet | ⛔ not yet | ⛔ not yet |

→ **Gap L1:** OtOpcUa already uses the right CSS classes but the component structure
  doesn't use `ThemeShell`. Low-risk migration.
→ **Gap L2:** MxAccessGateway and ScadaBridge use `.sidebar` / `.nav-link` / `<ul><li>`.
  Migration requires class name changes throughout their nav markup and `site.css` sidebar
  blocks. Medium (ScadaBridge) to high (MxGateway combined layout) risk.

### §4 Component contract

| Component | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| `StatusPill` (vs bespoke `StatusBadge`) | ⛔ `StatusBadge` (string CSS class) | ⛔ `StatusBadge` (string text → class) | ⛔ raw `.chip-*` classes inline |
| `LoginCard` | ⛔ inline markup in `Login.razor` | ⛔ no Blazor login page | ⛔ Bootstrap `.card` markup in `Login.razor` |
| `NavRailItem` / `NavRailSection` | ⛔ `NavLink` + `NavSection` (interactive) | ⛔ `NavLink`+`<li>` + `NavSection` | ⛔ `NavLink`+`<li>` + `NavSection` |
| `ThemeShell` / thin `MainLayout` | ⛔ not yet | ⛔ not yet | ⛔ not yet |
| `ThemeHead` | ⛔ manual `<link>` tags | ⛔ manual `<link>` tags | ⛔ manual `<link>` tags |

### §5 Delivery

| Item | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| Asset via `_content/ZB.MOM.WW.Theme/…` | ⛔ `_content/…AdminUI/css/…` | ⛔ root-relative `/css/…` | ⛔ `_content/…CentralUI/css/…` |
| `<ThemeHead />` in `<head>` | ⛔ manual `<link>` tags | ⛔ manual `<link>` tags | ⛔ manual `<link>` tags |

---

## Adoption backlog (ordered)

| # | Item | Projects | Priority | Effort | Risk | Notes |
|---|---|---|---|---|---|---|
| 1 | Build `ZB.MOM.WW.Theme` RCL | scadaproj | High | M | Low | **DONE** — `0.1.0` built + tested in this repo |
| 2 | Adopt in OtOpcUa AdminUI | OtOpcUa | High | S | Low | Already rail; fix latent font 404; cookie nav-state optional retain |
| 3 | Adopt in ScadaBridge CentralUI + Host | ScadaBridge | Med | M | Med | Sidebar class migration + `MainLayout` replace; scoped `.razor.css` unchanged |
| 4 | Adopt in MxAccessGateway Dashboard | MxAccessGateway | Low | L | High | Combined `MainLayout` migration; sidebar idiom change; largest UX-visible change — verify visually |

**Sequencing:** #2 first (lowest risk, validates the adoption pattern); #3 next (medium
effort, no design change); #4 last (highest risk — verify dashboard UX thoroughly before
merging). Each adoption is a per-repo PR, independent.

---

## Open questions

- **MxGateway login:** No Blazor login page today. If one is added during adoption (#4),
  use `<LoginCard>`. If the server-redirect pattern is kept, `<LoginCard>` is not needed.
- **OtOpcUa cookie nav state:** Decide whether to retain `otopcua_nav` cookie persistence
  (keep bespoke interactive `NavSection` alongside `ThemeShell`'s `Nav` slot) or drop it
  (CSS-only `NavRailSection` replaces it, losing expand-state persistence across page loads).
- **ScadaBridge `AuthorizeView` policy gating in nav:** Verify `<NavRailSection>` inside
  `<AuthorizeView>` renders + hides correctly with the canonical SSR rendering model.
