docs(ui-theme): approved adoption design (publish 0.2.0 + full canonical cutover across 3 apps)
This commit is contained in:
@@ -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`). | Low–Med |
|
||||
| **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).
|
||||
Reference in New Issue
Block a user