# 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 11–18),
  `<div class="collapse d-lg-block" id="sidebar-collapse">` (line 21), `<NavSidebar />` (line 22),
  `<main class="page">@Body</main>` (lines 25–27).

**`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 14–14).
- Nav sections: two `NavSection` groups ("Navigation", "Scripting", "Live", "Config")
  with `<NavLink class="rail-link">` children.
- Rail foot (lines 44–62): `<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 22–25), 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 17–18:
   ```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).
