From e6e9dbfedb2da21ecf2fade16d6d1e667927c022 Mon Sep 17 00:00:00 2001
From: Joseph Doherty
Date: Wed, 3 Jun 2026 02:35:00 -0400
Subject: [PATCH] docs(ui-theme): approved adoption design (publish 0.2.0 +
full canonical cutover across 3 apps)
---
.../2026-06-03-ui-theme-adoption-design.md | 171 ++++++++++++++++++
1 file changed, 171 insertions(+)
create mode 100644 docs/plans/2026-06-03-ui-theme-adoption-design.md
diff --git a/docs/plans/2026-06-03-ui-theme-adoption-design.md b/docs/plans/2026-06-03-ui-theme-adoption-design.md
new file mode 100644
index 0000000..4ad3c82
--- /dev/null
+++ b/docs/plans/2026-06-03-ui-theme-adoption-design.md
@@ -0,0 +1,171 @@
+# 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`/`- ` (`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 `
` + localStorage enhancer** (recommended over promoting OtOpcUa's interactive-island+cookie) |
+| D4 | MxGateway login | **Add a new `` 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 `` + localStorage enhancer:**
+
+- `NavRailSection` stays the static-SSR-friendly ``
+ it already is. It gains a stable **`Key`** parameter (default = a slug of `Title`) emitted
+ as a `data-nav-key` attribute on the ``.
+- 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 `` component (sibling to `ThemeHead`) emits
+ ``, placed before
+ `
`.
+
+**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`; `` in `App.razor` `
` after Bootstrap + ``
+before ``; **delete the app's `theme.css` + vendored IBM Plex `.woff2` fonts**; replace
+`MainLayout` with the thin delegation to ``; rebuild nav with
+`NavRailItem`/`NavRailSection`; `StatusBadge`→``; login → ``; **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, ``, server-validate `ReturnUrl`). | Low–Med |
+| **ScadaBridge** CentralUI + Host | `.sidebar`/`.nav-link`/`- ` (`NavMenu`+`NavSection`) → kit nav (class migration throughout). Verify `` policy-gated sections render/hide under static SSR (GAPS open Q). ``/`` go in Host's `App.razor`. `StatusBadge`/inline `.chip-*`→`StatusPill`. `Login.razor`+`LoginLayout`→`LoginCard`. | Med |
+| **MxGateway** Dashboard | Split combined ~210-line `MainLayout` → thin `MainLayout` + `` (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 `` 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)
+`` 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 ``); (5) local `StatusBadge`/`.chip-*` removed → ``;
+(6) login is `` (static POST, ``, 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).