Files
scadaproj/docs/plans/2026-06-03-ui-theme-adoption-design.md
T

172 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI-Theme Adoption — Design
**Date:** 2026-06-03
**Status:** Approved (brainstorming complete) — ready for `writing-plans`.
**Component:** UI Theme (`ZB.MOM.WW.Theme` shared RCL).
**Goal:** Adopt the shared `ZB.MOM.WW.Theme` Razor Class Library across all three sister
apps (OtOpcUa AdminUI, MxAccessGateway Dashboard, ScadaBridge CentralUI + Host) via a
**full canonical cutover** (SPEC §7), after first **promoting nav-expand persistence into
the kit** so every app gets it from one shared mechanism.
> This is the UI-theme analogue of the completed Auth+Audit normalization
> (`docs/plans/2026-06-02-auth-audit-normalization*.md`). It is **UI-only**: no data
> contracts, no DB migrations, no wire protocols. The dominant risk is **visual
> regression**, not data corruption.
---
## 0. Verified starting state (2026-06-03)
Independently verified (the component docs were optimistic — cf. memory
`component-status-claims-are-optimistic`):
- **Library is real but unpublished and unadopted.** `ZB.MOM.WW.Theme/` holds all 10
components + a Release `0.1.0` nupkg, but the Gitea feed returns **HTTP 404** for the
package and **no app references it**. The shared-contract's "Published to the Gitea NuGet
feed" is aspirational. → This is a clean **publish + adopt**.
- **Library is plain files tracked by `scadaproj`** (not a nested git repo) — library
changes commit in `scadaproj` (cf. memory `shared-libs-are-plain-files-not-nested-repos`).
- **Per-app surface** matches `components/ui-theme/GAPS.md`:
- **OtOpcUa AdminUI** — already side-rail (`.app-shell`/`.side-rail`/`.rail-link`);
interactive `NavSidebar` island (`@rendermode InteractiveServer`) holding `_expanded`,
persisted via JS interop (`window.navState.get/.set`) to the `otopcua_nav` cookie
(comma-separated section ids, 1-yr, `SameSite=Lax`); bespoke `StatusBadge`; static-POST
`Login.razor`; own `theme.css` + vendored fonts. *Lowest risk.*
- **ScadaBridge CentralUI** — `.sidebar`/`.nav-link`/`<ul><li>` (`NavMenu` + `NavSection`);
`Login.razor` + `LoginLayout`; own `theme.css`; Host owns `App.razor`. *Medium risk.*
- **MxAccessGateway Dashboard** — combined `MainLayout` (~210 lines); `.sidebar`/`.nav-link`;
`StatusBadge`; **no Blazor login page** (server-redirect); own `theme.css` (font path is
absolute `/fonts/…`, not portable). *Highest risk.*
---
## 1. Decisions (locked during brainstorming)
| # | Decision | Choice |
|---|---|---|
| D1 | Adoption depth | **A — Full canonical cutover** (SPEC §7 acceptance, all three apps) |
| D2 | Nav persistence | **On all apps, via one shared kit mechanism** (not bespoke per app) |
| D3 | Persistence implementation | **CSS `<details>` + localStorage enhancer** (recommended over promoting OtOpcUa's interactive-island+cookie) |
| D4 | MxGateway login | **Add a new `<LoginCard>` Blazor login page** (the higher-risk consistency option) |
| D5 | Delivery model | **Same as Auth/Audit**`feat/adopt-zb-theme` per app, local-only, then fast-forward merge to each repo's default + push to gitea on explicit go; scadaproj docs on `docs/ui-theme-adoption` |
| D6 | Publish | **Publish the (enhanced) RCL to the Gitea feed first**, then adopt (needs `GITEA_NUGET_KEY`, user-supplied, not persisted) |
| D7 | Library version | **Bump `0.1.0 → 0.2.0`** (new feature: persistent nav + `ThemeScripts`); publish `0.2.0` directly (0.1.0 was never released) |
| D8 | Accent colors | Preserve each app's current `--accent` value (move the *source* to the RCL, don't shift palettes) |
---
## 2. Program shape & sequencing
A **library-minor-then-adopt waterfall** (same shape as Auth/Audit):
- **Phase 0 — Library enhancement + publish.** Add shared nav persistence (§3), bump to
`0.2.0`, run the bUnit suite, `build/push.sh` to the Gitea feed. Commits in `scadaproj`.
- **Phase 1 — OtOpcUa AdminUI** (lowest risk; already side-rail; validates the pattern).
- **Phase 2 — ScadaBridge CentralUI + Host** (medium; class migration + AuthorizeView nav).
- **Phase 3 — MxAccessGateway Dashboard** (highest; split combined layout **and** add the
net-new `LoginCard` page).
- **Phase 4 — scadaproj docs + memory** (GAPS adoption banner; CLAUDE.md ui-theme row →
*Adopted*; shared-contract → *Published 0.2.0*; memory note).
**Execution:** subagent-driven, classification-driven reviews (trivial→none; small→code;
standard→spec∥code parallel; high-risk→serial spec→code + final integration review).
**Delivery:** `feat/adopt-zb-theme` branch per app, local-only; full build+test green per
repo; fast-forward merge to each default + push to gitea on the user's explicit go.
---
## 3. Library enhancement: shared nav persistence (Phase 0)
Promote **one** shared mechanism into the kit — a simpler generalization of OtOpcUa's
proven cookie+interop approach.
**Mechanism — CSS `<details>` + localStorage enhancer:**
- `NavRailSection` stays the static-SSR-friendly `<details class="rail-section" open="@Expanded">`
it already is. It gains a stable **`Key`** parameter (default = a slug of `Title`) emitted
as a `data-nav-key` attribute on the `<details>`.
- New vendored asset `wwwroot/js/nav-state.js` in the RCL: on `DOMContentLoaded`, for each
`[data-nav-key]`, read `localStorage` and set `el.open`; attach a `toggle` listener that
writes `el.open` back to `localStorage` keyed by `data-nav-key`. Pure client-side
progressive enhancement — no circuit, no server round-trip.
- New `<ThemeScripts/>` component (sibling to `ThemeHead`) emits
`<script src="_content/ZB.MOM.WW.Theme/js/nav-state.js" defer></script>`, placed before
`</body>`.
**Why localStorage over promoting OtOpcUa's island+cookie:** keeps the kit
**static-SSR-friendly** (no forced `InteractiveServer` island per app), one shared file,
uniform across all three. It *simplifies* OtOpcUa — retiring its interactive `NavSidebar`
island + `nav-state.js` + `otopcua_nav` cookie in favor of the shared enhancer. localStorage
is per-browser/origin (same effective scope as the old cookie) and is never read
server-side today, so nothing is lost.
**Trade-off:** a brief flash-of-default-state on first paint (localStorage isn't readable
server-side, so sections render at their server default and JS corrects after load).
Negligible for a nav rail. (If zero-flash were required, the alternative is a server-read
cookie — rejected as more kit coupling.)
**Version:** `0.1.0 → 0.2.0` (additive feature). **Tests:** extend the bUnit suite —
`NavRailSection` emits `data-nav-key` (derived slug + explicit `Key`); `ThemeScripts` emits
the script tag. JS runtime behavior is covered by the per-app manual checklist (§5), since
bUnit has no JS engine.
---
## 4. Per-app adoption scope (full canonical cutover)
Each app, per SPEC §7: add `PackageReference ZB.MOM.WW.Theme 0.2.0` + `@using ZB.MOM.WW.Theme`
in `_Imports.razor`; `<ThemeHead/>` in `App.razor` `<head>` after Bootstrap + `<ThemeScripts/>`
before `</body>`; **delete the app's `theme.css` + vendored IBM Plex `.woff2` fonts**; replace
`MainLayout` with the thin delegation to `<ThemeShell Product=… Accent=…>`; rebuild nav with
`NavRailItem`/`NavRailSection`; `StatusBadge``<StatusPill>`; login → `<LoginCard>`; **keep**
each app's `site.css` page-layout residual + scoped `.razor.css` unchanged. `--accent`
preserves each app's current value (D8).
| App | Notable specifics | Risk |
|---|---|---|
| **OtOpcUa** AdminUI | Already-correct rail classes (RCL `layout.css` matches). Retire `NavSidebar` island + `nav-state.js` + `otopcua_nav` cookie → kit `NavRailSection`/`NavRailItem` + shared enhancer. `RailFooter` = the existing `AuthorizeView` session block. `StatusBadge``StatusPill`. `Login.razor``LoginCard` (keep static POST, `<AntiforgeryToken/>`, server-validate `ReturnUrl`). | LowMed |
| **ScadaBridge** CentralUI + Host | `.sidebar`/`.nav-link`/`<ul><li>` (`NavMenu`+`NavSection`) → kit nav (class migration throughout). Verify `<AuthorizeView>` policy-gated sections render/hide under static SSR (GAPS open Q). `<ThemeHead/>`/`<ThemeScripts/>` go in Host's `App.razor`. `StatusBadge`/inline `.chip-*``StatusPill`. `Login.razor`+`LoginLayout``LoginCard`. | Med |
| **MxGateway** Dashboard | Split combined ~210-line `MainLayout` → thin `MainLayout` + `<ThemeShell>` (nav extracted into the `Nav` slot). `.sidebar`/`.nav-link`→rail classes; portable font path fixed by RCL. `StatusBadge``StatusPill`. **Add a new `/login` Blazor page** using `<LoginCard>` posting to a `/auth/login` endpoint wired to the app's existing `ZB.MOM.WW.Auth` LDAP service + dashboard cookie `SignInAsync` (mirror OtOpcUa/ScadaBridge static-POST login). Verify the server auth-redirect now lands on this page. | **High** |
---
## 5. Delivery, risk & verification
- **Build/test gate per repo:** `dotnet build` + the full suite green before merge. Baseline
the **known pre-existing reds** first and do not chase them (ScadaBridge IntegrationTests
×11 needing live LDAP/SQL/SMTP + flaky `StaleTagMonitor` timer tests; MxGateway 3 FakeWorker
tests) — only regressions introduced by this work count.
- **Visual regression is the real risk** — a green build does not prove the chrome looks
right. Verification per app = a structured manual checklist:
1. Rail renders at `lg`+ and collapses to a hamburger toggle below `lg`.
2. Nav expand-state persists across navigations and a full reload (shared enhancer).
3. `StatusPill` renders correctly in all five states (`Ok`/`Warn`/`Bad`/`Idle`/`Info`).
4. Login posts, round-trips `ReturnUrl` safely (server-validated), shows errors.
5. IBM Plex fonts load from `_content/ZB.MOM.WW.Theme/fonts/…` (no 404; OtOpcUa's latent
font 404 is fixed).
- **Optional browser smoke pass:** run each app locally and drive a Claude-in-Chrome smoke
pass (screenshots of shell + login) before merge — included only if the user opts in;
otherwise the checklist above is run manually.
- **MxGateway `/login`** is auth-facing and net-new → `high-risk` classification (serial
spec→code review + final integration review).
---
## 6. Acceptance (per app)
Mirrors SPEC §7: (1) `ZB.MOM.WW.Theme 0.2.0` referenced + in `_Imports.razor`; (2)
`<ThemeHead/>` after Bootstrap and per-app `theme.css`/fonts deleted; (3) `MainLayout` is the
thin `ThemeShell` delegation; (4) nav rebuilt with `NavRailItem`/`NavRailSection` (+ shared
persistence via `<ThemeScripts/>`); (5) local `StatusBadge`/`.chip-*` removed → `<StatusPill>`;
(6) login is `<LoginCard>` (static POST, `<AntiforgeryToken/>`, server-validated `ReturnUrl`)
— including MxGateway's net-new page; (7) `site.css` residual + scoped `.razor.css` kept.
---
## 7. Out of scope
Per SPEC §0/§6: each app's `site.css` page-layout residual, route/page content, scoped
`.razor.css`, authorization logic. The kit owns *chrome and tokens*, not domain screens.
No new data grids/modals/toasts (YAGNI). Bootstrap stays per-app (not vendored by the kit).