docs(ui-theme): approved adoption design (publish 0.2.0 + full canonical cutover across 3 apps)

This commit is contained in:
Joseph Doherty
2026-06-03 02:35:00 -04:00
parent 6d262f7d7c
commit e6e9dbfedb
@@ -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`/`<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).