# 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. > **βœ… 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): ``/``, > thin `MainLayout` β†’ `` + `NavRailItem`/`NavRailSection`, per-app `theme.css`/IBM-Plex fonts/ > `nav-state.js` deleted, `` 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 > ``), 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 `` `/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 | βœ… `` (NavLink) | β›” `
  • ` inside `
      ` | β›” `
    • ` inside `
        ` | | 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` / `
        • `. 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`+`
        • ` + `NavSection` | β›” `NavLink`+`
        • ` + `NavSection` | | `ThemeShell` / thin `MainLayout` | β›” not yet | β›” not yet | β›” not yet | | `ThemeHead` | β›” manual `` tags | β›” manual `` tags | β›” manual `` tags | ### Β§5 Delivery | Item | OtOpcUa | MxAccessGateway | ScadaBridge | |---|---|---|---| | Asset via `_content/ZB.MOM.WW.Theme/…` | β›” `_content/…AdminUI/css/…` | β›” root-relative `/css/…` | β›” `_content/…CentralUI/css/…` | | `` in `` | β›” manual `` tags | β›” manual `` tags | β›” manual `` 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 ``. If the server-redirect pattern is kept, `` 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 `` inside `` renders + hides correctly with the canonical SSR rendering model.