docs(ui-theme): current-state ×3 + GAPS adoption backlog

This commit is contained in:
Joseph Doherty
2026-06-01 05:15:38 -04:00
parent 95975d0754
commit 029ac0719b
4 changed files with 579 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# 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.
---
## 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.
@@ -0,0 +1,161 @@
# UI Theme — current state: MxAccessGateway
Repo: `~/Desktop/MxAccessGateway` (Gitea `mxaccessgw`). Stack: .NET 10, Blazor SSR
(gateway x64) — UI in `src/ZB.MOM.WW.MxGateway.Server/`.
All paths below are relative to the repo root. Verified against source on 2026-06-01.
**Summary:** MxAccessGateway uses a sidebar nav layout and the Technical-Light tokens, but
the sidebar uses Bootstrap `.sidebar` / `.nav-link` classes rather than the canonical
`.side-rail` / `.rail-link` classes, and the overall structure diverges from the spec
target. Adoption has the **highest effort and risk** of the three apps — the shell
requires migration from its current sidebar idiom to the canonical `ThemeShell` pattern.
There is no dedicated login page (authentication gate is integrated into the Dashboard).
---
## 1. CSS / design tokens
**`theme.css`** — 379-line hand copy of the Technical-Light design system. Identical in
content to OtOpcUa's and ScadaBridge's copies except for the font-path prefix.
- Path: `src/ZB.MOM.WW.MxGateway.Server/wwwroot/css/theme.css`
- Font path: `url('/fonts/ibm-plex-sans-400.woff2')` (lines 24, 29, 34)
- Absolute path (`/fonts/…`) is technically correct (resolves from root of the app), but
differs from the canonical `url('../fonts/…')` in the RCL — a deployment path difference,
not a loading bug.
- Wired in `App.razor` line 6: `<link rel="stylesheet" href="/css/theme.css" />`.
**`site.css`** — 592 lines of per-app page layout and Dashboard component styling.
- Path: `src/ZB.MOM.WW.MxGateway.Server/wwwroot/css/site.css`
- Wired in `App.razor` line 7: `<link rel="stylesheet" href="/css/site.css" />`.
- Contains: `.sidebar` layout (lines ~2495), `.dashboard-body`, `.agg-card`, table
styles, metric cards, event/alarm grids, and other domain-specific rules.
- After adoption: the `.sidebar` layout section is superseded by RCL `layout.css`.
The domain-specific table/card/grid rules stay in `site.css`.
Note: MxGateway's `App.razor` loads assets from `/css/…` and `/fonts/…` (root-relative
paths to `wwwroot/`), not via `_content/…` static-web-asset paths — contrast with
OtOpcUa and ScadaBridge which use the RCL `_content/` mechanism.
---
## 2. IBM Plex fonts
Three `.woff2` files vendored into:
`src/ZB.MOM.WW.MxGateway.Server/wwwroot/fonts/`
- `ibm-plex-sans-400.woff2`
- `ibm-plex-sans-600.woff2`
- `ibm-plex-mono-500.woff2`
After adoption: delete all three; the RCL serves them from
`_content/ZB.MOM.WW.Theme/fonts/`.
---
## 3. Layout shell
**`MainLayout.razor`** — 210-line combined layout + nav component. `@implements
IDisposable`; `@inject NavigationManager`, `@inject IJSRuntime`. Interactive
(`@rendermode InteractiveServer` inherited from `Routes`).
- Path: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/MainLayout.razor`
- Root element: `<div class="d-flex flex-column flex-lg-row" style="min-height: 100vh;">`.
Note: **no `.app-shell` class** (unlike OtOpcUa and the spec target).
- Brand: `<a class="brand" href="/"><span class="mark">&#9646;</span> MXAccess Gateway</a>`
(line 24).
- Nav structure: `<nav class="sidebar d-flex flex-column">` with `<ul class="nav flex-column">`
and `<NavSection>` groups ("Runtime", "Galaxy", "Admin", "Configuration") with
`<NavLink class="nav-link">` children (not `.rail-link`).
- No dedicated `RailFooter` / session block (auth state shown elsewhere or via API keys).
- Nav state persisted via JS (`nav-state.js`), same pattern as OtOpcUa.
**`NavSection.razor`** — 40-line component using `EventCallback OnToggle` + JS collapse.
- Path: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/NavSection.razor`
- Structure: `<li class="nav-item"><button class="nav-section-toggle" @onclick="OnToggle">`.
Uses Bootstrap-style `<ul>/<li>` nav items, not `.rail-link` anchor style.
---
## 4. Login / auth surface
MxAccessGateway has **no dedicated Blazor login page**. There is no `Login.razor`. The
dashboard is protected by ASP.NET Core cookie authentication; login is handled via an
ASP.NET Minimal API auth endpoint (outside the Blazor component tree). The `MainLayout`
includes a "Sign In" link (`<a href="/login" class="btn …">Sign In</a>` line 87) that
redirects to the server endpoint. The `<LoginCard>` component is not applicable until
a Blazor login page is added.
---
## 5. StatusBadge component
**`StatusBadge.razor`** — string-matchbased chip component.
- Path: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Shared/StatusBadge.razor`
- Parameters: `string? Text`. Maps known strings ("Ready", "Healthy", "Active", "Faulted",
etc.) to `chip-ok` / `chip-warn` / `chip-bad` / `chip-idle` CSS classes.
- After adoption: the string-matching logic is app-specific (based on gateway session
state strings). Migration to `StatusPill` requires the caller to map gateway state
strings to `StatusState` values, then pass `State` instead of `Text`.
---
## 6. Divergences from spec
| Item | Current state | Spec |
|---|---|---|
| `theme.css` | Hand copy, 379 lines | Single canonical copy in RCL |
| Font-path `url()` | `url('/fonts/…')` (absolute, not a bug but non-canonical) | `url('../fonts/…')` |
| IBM Plex fonts | Vendored 3× in `wwwroot/fonts/` | Single copy in RCL `wwwroot/fonts/` |
| Asset wiring | Root-relative `/css/…`, `/fonts/…` | `_content/ZB.MOM.WW.Theme/…` |
| Shell class | `d-flex …` (no `.app-shell`) | `ThemeShell` + `.app-shell` |
| Nav class idiom | `.sidebar` + `.nav-link` + `<ul>/<li>` | `.side-rail` + `.rail-link` + `<a>` |
| Nav items | `<NavLink class="nav-link">` inside `<li>` | `<NavRailItem>` |
| Nav sections | `NavSection` (button `OnToggle` + JS) | `NavRailSection` (`<details>`, CSS-only) |
| Status chip | `StatusBadge` (string text → CSS class) | `StatusPill` (`StatusState` enum) |
| Login page | None — server endpoint redirect only | `<LoginCard>` (if a Blazor login page is added) |
---
## 7. Adoption plan
**Effort: High. Risk: High.** The sidebar idiom (`nav.sidebar` + `.nav-link` + `<ul><li>`)
differs from the canonical rail idiom (`.side-rail` + `.rail-link` + `<a>`), requiring a
CSS and markup migration. No layout redesign (it already uses a side-panel pattern), but
class names, element structure, and the `site.css` sidebar block all change.
**Steps:**
1. **Delete copies.** Remove `wwwroot/css/theme.css` and `wwwroot/fonts/ibm-plex-*.woff2`
from `src/ZB.MOM.WW.MxGateway.Server/`.
2. **Reference RCL.** Add `<PackageReference Include="ZB.MOM.WW.Theme" />` to
`ZB.MOM.WW.MxGateway.Server.csproj`. Add `@using ZB.MOM.WW.Theme` to `_Imports.razor`.
3. **Wire `ThemeHead`.** In `App.razor` replace `/css/theme.css` link with `<ThemeHead />`.
Keep `/css/site.css` for domain-specific rules. Also change static asset paths from
root-relative to `_content/ZB.MOM.WW.Theme/…` for fonts if any remain in `site.css`.
4. **Replace `MainLayout`.** Replace the 210-line `MainLayout.razor` with a thin wrapper
around `<ThemeShell Product="MXAccess Gateway">`. Carry the nav sections and the sign-in
link into the `Nav` and `RailFooter` slots respectively.
5. **Port nav items.** Migrate from `<NavLink class="nav-link">` inside `<li class="nav-item">`
to `<NavRailItem Href="…" Text="…">`. The four section groups ("Runtime", "Galaxy",
"Admin", "Configuration") map to `<NavRailSection Title="…">` children.
6. **Clean `site.css`.** Remove the `.sidebar` layout block (lines ~2495) — superseded by
`layout.css`. Keep all dashboard/domain-specific rules (`.agg-card`, tables, metric
cards, etc.).
7. **Replace `StatusBadge`.** Add a helper that maps gateway session-state strings to
`StatusState` values; replace `<StatusBadge Text="…">` call sites with
`<StatusPill State="…">`. Delete `StatusBadge.razor`.
8. **Login card (optional).** No Blazor login page exists today. If one is added,
`<LoginCard>` is the canonical implementation.
9. **Keep:** domain-specific `site.css` rules, scoped `.razor.css` files (none currently),
API-key authentication, all page components.
**Flagged risk:** This is the largest UX-visible change across the three apps. The sidebar
class migration (`.sidebar``.side-rail`, `.nav-link``.rail-link`) will visually
change the nav styling. Verify visually in the dashboard before merging. The nav expand
state persistence (JS-based) must be verified or replaced with CSS-only `<details>`.
@@ -0,0 +1,163 @@
# UI Theme — current state: OtOpcUa
Repo: `~/Desktop/OtOpcUa` (Gitea `lmxopcua`). Stack: .NET 10, Blazor SSR.
UI surface: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/` (Razor Class Library).
All paths below are relative to the repo root. Verified against source on 2026-06-01.
**Summary:** OtOpcUa already uses a side-rail layout and the full Technical-Light token
set. Adoption is **lowest effort** of the three apps — the shell shape already matches the
canonical target. The one bug fixed by adoption: a latent font-path 404 that silently
falls back to system fonts today.
---
## 1. CSS / design tokens
**`theme.css`** — 379-line hand copy of the Technical-Light design system.
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/wwwroot/css/theme.css`
- Font path: `url('fonts/ibm-plex-sans-400.woff2')` (lines 24, 29, 34)
- **Bug:** the path is relative to the CSS file location (`wwwroot/css/`), so it resolves
as `wwwroot/css/fonts/…` — a 404. The browser silently falls back to system fonts. The
canonical RCL path `url('../fonts/…')` fixes this permanently.
- Wired in `App.razor` line 17:
`<link rel="stylesheet" href="_content/ZB.MOM.WW.OtOpcUa.AdminUI/css/theme.css"/>`.
**`site.css`** — 174 lines of per-app page layout (side-rail shell, login card layout,
page body padding, miscellaneous overrides).
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/wwwroot/css/site.css`
- Wired in `App.razor` line 18:
`<link rel="stylesheet" href="_content/ZB.MOM.WW.OtOpcUa.AdminUI/css/site.css"/>`.
- After adoption: the `.side-rail`, `.rail-*`, `.login-wrap`, `.login-title` rules are
superseded by the RCL's `layout.css`. The page-layout residuals (body padding, page-
specific overrides) stay in `site.css`.
---
## 2. IBM Plex fonts
Three `.woff2` files vendored into:
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/wwwroot/fonts/`
- `ibm-plex-sans-400.woff2`
- `ibm-plex-sans-600.woff2`
- `ibm-plex-mono-500.woff2`
After adoption: delete all three; the RCL serves them from
`_content/ZB.MOM.WW.Theme/fonts/`.
---
## 3. Layout shell
**`MainLayout.razor`** — 28-line static layout (no `@rendermode`). Renders `.app-shell`
flex row, hamburger toggle, `<NavSidebar/>` inside a Bootstrap collapse div, and
`<main class="page">`.
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/MainLayout.razor`
- Structure: `.app-shell d-flex flex-column flex-lg-row` (line 8), hamburger (lines 1118),
`<div class="collapse d-lg-block" id="sidebar-collapse">` (line 21), `<NavSidebar />` (line 22),
`<main class="page">@Body</main>` (lines 2527).
**`NavSidebar.razor`** — 160-line interactive (`@rendermode InteractiveServer`) sidebar.
Hosts the collapsible `NavSection` groups and cookie-persisted expand state.
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/NavSidebar.razor`
- Brand: `<div class="brand"><span class="mark">&#9646;</span> OtOpcUa</div>` (lines 1414).
- Nav sections: two `NavSection` groups ("Navigation", "Scripting", "Live", "Config")
with `<NavLink class="rail-link">` children.
- Rail foot (lines 4462): `<div class="rail-foot"><AuthorizeView>` — session info + sign-out
`<form method="post" action="/auth/logout">`.
- Nav expand state persisted in `otopcua_nav` cookie via
`wwwroot/js/nav-state.js` (cookie: `otopcua_nav=<comma-separated ids>`).
**`NavSection.razor`** — 36-line `NavSection` component (interactive; uses `EventCallback`
`OnToggle` for expand/collapse, not CSS `<details>`).
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/NavSection.razor`
**`LoginLayout.razor`** — plain layout (no sidebar) used by the login page.
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/LoginLayout.razor`
---
## 4. Login page
**`Login.razor`** — 50-line static login page. Uses `@layout LoginLayout`,
`@attribute [AllowAnonymous]`.
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Login.razor`
- Form: `<form method="post" action="/auth/login" data-enhance="false">` (line 21).
- Hidden `returnUrl` input (line 2225), username/password inputs, error notice panel.
- The form structure exactly matches what `<LoginCard>` emits; migration is direct.
---
## 5. StatusBadge component
**`StatusBadge.razor`** — thin wrapper over `.chip` classes.
- Path: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/StatusBadge.razor`
- Parameters: `string Text`, `string CssClass` (default `chip-idle`).
- After adoption: replaced by `<StatusPill State="…">` — caller maps state to `StatusState`
enum rather than passing CSS class strings directly.
---
## 6. Divergences from spec
| Item | Current state | Spec |
|---|---|---|
| `theme.css` | Hand copy, 379 lines | Single canonical copy in RCL |
| Font-path `url()` | `url('fonts/…')`**latent 404** | `url('../fonts/…')` — correct |
| IBM Plex fonts | Vendored 3× in `wwwroot/fonts/` | Single copy in RCL `wwwroot/fonts/` |
| Shell layout | `.app-shell` + `NavSidebar` component (matches target shape) | `ThemeShell` + thin `MainLayout` |
| Nav items | `<NavLink class="rail-link">` inside interactive `NavSidebar` | `NavRailItem` inside `NavRailSection` |
| Nav expand state | Cookie-persisted via `otopcua_nav` + JS | CSS-only `<details>` in `NavRailSection` |
| Status chip | `StatusBadge` (string CSS class param) | `StatusPill` (`StatusState` enum param) |
| Login card | Inline markup in `Login.razor` | `<LoginCard>` |
---
## 7. Adoption plan
**Effort: Low.** The shell shape already matches the target. No layout migration needed.
**Steps:**
1. **Delete copies.** Remove `wwwroot/css/theme.css` and `wwwroot/fonts/ibm-plex-*.woff2`
from `ZB.MOM.WW.OtOpcUa.AdminUI`. This also fixes the latent font-path 404.
2. **Reference RCL.** Add `<PackageReference Include="ZB.MOM.WW.Theme" />` to
`ZB.MOM.WW.OtOpcUa.AdminUI.csproj`. Add `@using ZB.MOM.WW.Theme` to `_Imports.razor`.
3. **Wire `ThemeHead`.** In `App.razor` replace lines 1718:
```diff
- <link rel="stylesheet" href="_content/ZB.MOM.WW.OtOpcUa.AdminUI/css/theme.css"/>
- <link rel="stylesheet" href="_content/ZB.MOM.WW.OtOpcUa.AdminUI/css/site.css"/>
+ <ThemeHead />
+ <link rel="stylesheet" href="_content/ZB.MOM.WW.OtOpcUa.AdminUI/css/site.css"/>
```
(Keep `site.css` for the page-layout residuals.)
4. **Replace `MainLayout`.** Delete the current 28-line `MainLayout.razor`. Create a new
thin `MainLayout.razor` that delegates to `<ThemeShell Product="OtOpcUa Admin">` with
`Nav` and `RailFooter` slots (carry the session/sign-out block from `NavSidebar`'s
`.rail-foot` into `RailFooter`).
5. **Port nav.** Rebuild the `Nav` slot using `<NavRailSection>` + `<NavRailItem>`. The
four section groups ("Navigation", "Scripting", "Live", "Config") map directly to
`NavRailSection Title="…"` with `NavRailItem` children.
**Cookie nav state:** OtOpcUa's `otopcua_nav` cookie persistence requires JS and an
`InteractiveServer` component. If this feature is retained, keep a bespoke interactive
`NavSection` (the current `NavSection.razor` or a refactored version) alongside — it
is compatible with `ThemeShell`'s `Nav` slot. If cookie persistence is acceptable to
drop, `NavRailSection` (CSS-only `<details>`) is a drop-in replacement.
6. **Replace `StatusBadge`.** Find all usages of `<StatusBadge CssClass="chip-*">` and
replace with `<StatusPill State="StatusState.*">`. Delete `StatusBadge.razor`.
7. **Replace login card.** In `Login.razor`, replace the inline `<div class="login-wrap">
… </div>` block with `<LoginCard Product="OtOpcUa Admin" Action="/auth/login"
ReturnUrl="@ReturnUrl" Error="@Error"><AntiforgeryToken /></LoginCard>`. The code-behind
(`Error` / `ReturnUrl` supply-from-query properties) stays unchanged.
8. **Keep:** `site.css` page-layout residuals; scoped `.razor.css` files (none currently in
AdminUI); `LoginLayout.razor`; auth endpoints; all page components.
**Risk: Low** — layout shape already matches, no top-bar migration. Cookie nav state is
the only optional complexity (decide retain vs drop).
@@ -0,0 +1,165 @@
# UI Theme — current state: ScadaBridge
Repo: `~/Desktop/ScadaBridge`. Stack: .NET 10, Blazor SSR (Akka.NET cluster + central UI).
UI surfaces: `src/ZB.MOM.WW.ScadaBridge.CentralUI/` (RCL) and
`src/ZB.MOM.WW.ScadaBridge.Host/` (the Blazor host that references it).
All paths below are relative to the repo root. Verified against source on 2026-06-01.
**Summary:** ScadaBridge uses a sidebar nav layout and the Technical-Light tokens, with the
correct font-path prefix. The sidebar uses `.sidebar` / `.nav-link` classes (same idiom as
MxGateway), not `.side-rail` / `.rail-link`. Adoption is **medium effort** — sidebar-class
migration + `MainLayout` replacement, no layout redesign. ScadaBridge has several
scoped `.razor.css` files that stay per-project.
---
## 1. CSS / design tokens
**`theme.css`** — 379-line hand copy of the Technical-Light design system.
- Path: `src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/theme.css`
- Font path: `url('../fonts/ibm-plex-sans-400.woff2')` (lines 24, 29, 34)
- **Correct path** — resolves from `wwwroot/css/` to `wwwroot/fonts/` without 404. This is
the canonical `url('../fonts/…')` that the RCL also uses.
- Wired in the Host's `App.razor` line 9:
`<link href="_content/ZB.MOM.WW.ScadaBridge.CentralUI/css/theme.css" rel="stylesheet" />`.
**`site.css`** — 128 lines of per-app page layout (sidebar shell, nav overrides).
- Path: `src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css`
- Wired in the Host's `App.razor` line 11:
`<link href="_content/ZB.MOM.WW.ScadaBridge.CentralUI/css/site.css" rel="stylesheet" />`.
- Contains: `.sidebar` layout block (~495), Bootstrap-icons integration for nav items.
- After adoption: the `.sidebar` layout section is superseded by RCL `layout.css`. The
remaining rules (Bootstrap-icons, misc overrides) stay in `site.css`.
Note: ScadaBridge uses the `_content/ZB.MOM.WW.ScadaBridge.CentralUI/…` static-web-asset
path for its own CentralUI RCL assets — the same mechanism `ZB.MOM.WW.Theme` will use.
---
## 2. IBM Plex fonts
Three `.woff2` files vendored into:
`src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/fonts/`
- `ibm-plex-sans-400.woff2`
- `ibm-plex-sans-600.woff2`
- `ibm-plex-mono-500.woff2`
After adoption: delete all three from `CentralUI/wwwroot/fonts/`; the RCL serves them
from `_content/ZB.MOM.WW.Theme/fonts/`.
---
## 3. Layout shell
**`MainLayout.razor`** — 29-line static layout. `@inherits LayoutComponentBase`. No
`@rendermode` directive (static SSR).
- Path: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/MainLayout.razor`
- Root element: `<div class="d-flex flex-column flex-lg-row" style="min-height: 100vh;">`.
No `.app-shell` class.
- Renders `<NavMenu />` inside a Bootstrap collapse div, `<main class="flex-grow-1 p-3">`,
plus `<DialogHost />` and `<SessionExpiry />` at the bottom.
**`NavMenu.razor`** — 200+ line interactive sidebar component. `@implements IDisposable`.
- Path: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor`
- Brand: `<div class="brand"><span class="mark">&#9646;</span> ScadaBridge</div>` (lines ~99).
- Nav structure: `<nav class="sidebar d-flex flex-column">` with `<ul class="nav flex-column">`
and `<NavSection>` groups ("Admin", "Data", "Audit", etc.) with `<NavLink class="nav-link">`
children. Uses `AuthorizeView` + `AuthorizeView Policy="…"` to gate admin sections.
- Nav state: JS-based expand-state persistence (same pattern as OtOpcUa and MxGateway).
**`NavSection.razor`** (same name as OtOpcUa/MxGateway, independent per-project copy).
- Path: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavSection.razor`
---
## 4. Login page
**`Login.razor`** — 36-line static login page. `@layout LoginLayout`, `@attribute [AllowAnonymous]`.
- Path: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Login.razor`
- Form: `<form method="post" action="/auth/login" data-enhance="false">` (line 16).
- Uses Bootstrap `.card` / `.card-body` markup — **not** the Technical-Light `.panel` /
`.login-wrap` idiom used in OtOpcUa. Does not use a `<LoginCard>` yet.
- Error notice: Bootstrap `.alert alert-danger` (line 12) rather than `.panel.notice`.
---
## 5. Scoped `.razor.css` files (stays per-project)
ScadaBridge ships several component-scoped CSS files. These are **not shared** and stay
in the CentralUI RCL after adoption:
| File | Path |
|---|---|
| `MultiSelectDropdown.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/` |
| `TreeView.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/` |
| `AuditDrilldownDrawer.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/` |
| `AuditEventDetail.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/` |
| `ExecutionDetailModal.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/` |
| `ExecutionTree.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/` |
| `AuditResultsGrid.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/` |
| `NodeBrowserDialog.razor.css` | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/` |
These scoped styles are component-specific overrides and are unaffected by theme adoption.
---
## 6. Divergences from spec
| Item | Current state | Spec |
|---|---|---|
| `theme.css` | Hand copy, 379 lines | Single canonical copy in RCL |
| Font-path `url()` | `url('../fonts/…')`**correct** | `url('../fonts/…')` — same |
| IBM Plex fonts | Vendored 3× in `wwwroot/fonts/` | Single copy in RCL `wwwroot/fonts/` |
| Shell class | `d-flex …` (no `.app-shell`) | `ThemeShell` + `.app-shell` |
| Nav class idiom | `.sidebar` + `.nav-link` + `<ul>/<li>` | `.side-rail` + `.rail-link` + `<a>` |
| Nav items | `<NavLink class="nav-link">` inside `<li>` | `<NavRailItem>` |
| Nav sections | `NavSection` (`EventCallback OnToggle` + JS) | `NavRailSection` (`<details>`, CSS-only) |
| Status chip | None (uses raw `.chip-*` classes inline) | `StatusPill` (`StatusState` enum) |
| Login card | Bootstrap `.card` markup (not Technical-Light `.panel`) | `<LoginCard>` |
| Scoped `.razor.css` | 8 component-scoped files | Stays per-project (no change) |
---
## 7. Adoption plan
**Effort: Medium. Risk: Medium.** The font path is already correct so no 404 fix needed.
The sidebar idiom migration (`.sidebar``.side-rail`, `.nav-link``.rail-link`) and the
`MainLayout` replacement are the main work. The scoped `.razor.css` files are unaffected.
**Steps:**
1. **Delete copies.** Remove `src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/theme.css`
and `wwwroot/fonts/ibm-plex-*.woff2` from `CentralUI`.
2. **Reference RCL.** Add `<PackageReference Include="ZB.MOM.WW.Theme" />` to
`ZB.MOM.WW.ScadaBridge.CentralUI.csproj`. Add `@using ZB.MOM.WW.Theme` to
`CentralUI`'s `_Imports.razor`.
3. **Wire `ThemeHead`.** In `Host`'s `App.razor`, replace line 9
(`<link href="_content/…/css/theme.css">`) with `<ThemeHead />` (which now resolves
via `ZB.MOM.WW.Theme`). Keep the `site.css` link on line 11.
4. **Replace `MainLayout`.** Replace the 29-line `MainLayout.razor` with a thin wrapper
around `<ThemeShell Product="ScadaBridge">`. Carry `<NavMenu />` into the `Nav` slot
(or replace it — see step 5). Keep `<DialogHost />` and `<SessionExpiry />` below the
`ThemeShell` or inside `ChildContent` as needed.
5. **Port nav.** Migrate `NavMenu.razor` from `nav.sidebar` + `<ul>/<li>` + `.nav-link`
to `<NavRailSection>` + `<NavRailItem>`. The `AuthorizeView` policy gating on admin
sections stays — wrap `<NavRailSection>` inside the appropriate `<AuthorizeView>` just
as today.
6. **Clean `site.css`.** Remove the `.sidebar` layout block. Keep Bootstrap-icons
integration and any domain-specific overrides.
7. **Replace login card.** In `Login.razor`, replace the Bootstrap `.card`/`.card-body`
markup with `<LoginCard Product="ScadaBridge" Action="/auth/login" ReturnUrl="@ReturnUrl"
Error="@ErrorMessage"><AntiforgeryToken /></LoginCard>`. Align error display with the
Technical-Light `.panel.notice` style from `LoginCard`.
8. **Keep:** all scoped `.razor.css` files (8 files listed above); `site.css` domain rules;
auth and session endpoints; all page components.
**Risk note:** ScadaBridge's `AuthorizeView` policy-gated nav sections require careful
testing — verify that `<NavRailSection>` inside `<AuthorizeView>` renders correctly and
that the section is fully hidden when the policy fails (not just collapsed).