# UI-Theme Adoption Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Enhance the shared `ZB.MOM.WW.Theme` RCL with cross-app nav-expand persistence (bump `0.2.0`, publish to the Gitea feed), then adopt it via full canonical cutover across OtOpcUa AdminUI, ScadaBridge CentralUI+Host, and MxAccessGateway Dashboard. **Architecture:** A library-minor-then-adopt waterfall (same shape as the completed Auth/Audit normalization). Phase 0 enhances + publishes the kit. Phases 1–3 are **independent per-repo cutovers** (each on its own `feat/adopt-zb-theme` branch, local-only) ordered by risk. Phase 4 updates scadaproj docs + memory. UI-only — no data contracts, no DB migrations; the dominant risk is **visual regression**, mitigated by per-app build+test gates and a manual visual checklist. **Tech Stack:** .NET 10, Blazor SSR, Razor Class Library, bUnit/xUnit, Bootstrap 5, NuGet central package management (OtOpcUa/ScadaBridge) / per-project versions (MxGateway), Gitea NuGet feed. **Design:** [`2026-06-03-ui-theme-adoption-design.md`](2026-06-03-ui-theme-adoption-design.md). Decisions D1–D8 there are authoritative. --- ## Conventions for the executor - **Delivery:** scadaproj library + docs changes (Phases 0, 4) commit on the existing `docs/ui-theme-adoption` branch. Each app (Phases 1–3) gets its own `feat/adopt-zb-theme` branch, **committed local-only, never pushed** until the user explicitly authorizes merge+push (same model as Auth/Audit). - **Per-repo green gate:** before declaring an app's phase done, run `dotnet build` + that repo's full test suite. **Baseline known pre-existing reds first** and do not chase them: ScadaBridge IntegrationTests ×11 (need live LDAP/SQL/SMTP), `PartitionPurgeTests.EndToEnd`, flaky `StaleTagMonitor` timer tests; MxGateway 3 FakeWorker tests. Only regressions introduced by this work count. - **Cutover invariant (all apps):** the kit's `theme.css`/`layout.css` define `--*` tokens, the side-rail layout, and the `.chip`/`.chip-ok|warn|bad|idle|info` status classes. Before deleting an app's `wwwroot/css/theme.css`, **diff it against the kit's `theme.css`/`layout.css` and migrate any app-only rules** (e.g. OtOpcUa's `.chip-alert`/`.chip-caution`) into that app's `site.css`. The app's `site.css` page-layout residual and scoped `.razor.css` stay. - **Status policy (per SPEC §6/§7):** inline `.chip-*` spans and Bootstrap `.badge` in *domain pages* are page content — they keep working under kit CSS and are **not** rewritten. Only a bespoke status *component* gets removed/redirected to ``. - **Cross-repo parallelism:** Phases 1, 2, 3 touch disjoint repos and are mutually independent — they MAY run concurrently, but are listed in risk order (OtOpcUa → ScadaBridge → MxGateway). All three are blocked by Task 0.4 (published package). --- ## Phase 0 — Library enhancement + publish (scadaproj, branch `docs/ui-theme-adoption`) ### Task 0.1: NavRailSection persistence key **Classification:** standard **Estimated implement time:** ~4 min **Parallelizable with:** Task 0.2 **Files:** - Modify: `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor` - Test: `ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/NavRailTests.cs` **Context:** `NavRailSection` renders `
@Title…`. Add an optional `Key` parameter (default = a stable slug of `Title`) emitted as `data-nav-key` on the `
` so the localStorage enhancer (Task 0.2) can persist per-section open state. **Step 1 — failing tests** in `NavRailTests.cs`: ```csharp [Fact] public void NavRailSection_emits_data_nav_key_slug_from_title_by_default() { var cut = RenderComponent(p => p .Add(x => x.Title, "Site Calls") .AddChildContent("X")); Assert.Equal("site-calls", cut.Find("details.rail-section").GetAttribute("data-nav-key")); } [Fact] public void NavRailSection_emits_explicit_key_when_supplied() { var cut = RenderComponent(p => p .Add(x => x.Title, "Navigation").Add(x => x.Key, "nav") .AddChildContent("X")); Assert.Equal("nav", cut.Find("details.rail-section").GetAttribute("data-nav-key")); } ``` **Step 2 — run, expect FAIL** (no `Key`/`data-nav-key`): `dotnet test ZB.MOM.WW.Theme/ --filter "FullyQualifiedName~NavRailSection_emits"` **Step 3 — implement.** Edit `NavRailSection.razor`: ```razor @namespace ZB.MOM.WW.Theme
@Title
@ChildContent
@code { [Parameter, EditorRequired] public string Title { get; set; } = string.Empty; [Parameter] public bool Expanded { get; set; } = true; /// Stable identifier used to persist this section's open/closed state in /// localStorage (via the kit's nav-state.js). Defaults to a slug of . [Parameter] public string? Key { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } private string ResolvedKey => string.IsNullOrWhiteSpace(Key) ? Slug(Title) : Key!; private static string Slug(string s) { var chars = s.Trim().ToLowerInvariant() .Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray(); return string.Join('-', new string(chars).Split('-', StringSplitOptions.RemoveEmptyEntries)); } } ``` **Step 4 — run, expect PASS** (plus the existing NavRail tests stay green). **Step 5 — commit:** `git add -A && git commit -m "feat(theme): NavRailSection data-nav-key for persistence"` --- ### Task 0.2: localStorage nav enhancer + ThemeScripts **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 0.1 **Files:** - Create: `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/js/nav-state.js` - Create: `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/ThemeScripts.razor` - Test: `ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/ThemeScriptsTests.cs` (new) - Test: `ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/StaticAssetsTests.cs` (extend) **Step 1 — create `wwwroot/js/nav-state.js`** (progressive enhancement; no framework): ```javascript // ZB.MOM.WW.Theme nav-state.js — persists
open/closed // state in localStorage so NavRailSection expand state survives navigation and // reloads. Pure client-side; works with static Blazor SSR. Keyed per section. (function () { var PREFIX = "zbnav:"; function apply() { document.querySelectorAll("details.rail-section[data-nav-key]").forEach(function (el) { var key = PREFIX + el.getAttribute("data-nav-key"); var saved = null; try { saved = window.localStorage.getItem(key); } catch (e) { return; } if (saved === "1") el.open = true; else if (saved === "0") el.open = false; el.addEventListener("toggle", function () { try { window.localStorage.setItem(key, el.open ? "1" : "0"); } catch (e) { /* ignore */ } }); }); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", apply); else apply(); })(); ``` **Step 2 — create `Components/ThemeScripts.razor`:** ```razor @namespace ZB.MOM.WW.Theme @* Components/ThemeScripts.razor — drop before . Emits the kit's nav-state enhancer that persists NavRailSection open/closed state in localStorage. *@ ``` **Step 3 — failing tests.** `ThemeScriptsTests.cs`: ```csharp namespace ZB.MOM.WW.Theme.Tests; public class ThemeScriptsTests : TestContext { [Fact] public void ThemeScripts_emits_nav_state_script_tag() { var cut = RenderComponent(); var script = cut.Find("script"); Assert.Equal("_content/ZB.MOM.WW.Theme/js/nav-state.js", script.GetAttribute("src")); Assert.True(script.HasAttribute("defer")); } } ``` In `StaticAssetsTests.cs`, add an assertion that the JS file ships (mirror its existing CSS/font asset checks — read the file first to match its exact assertion style, e.g. verifying the file exists on disk under `wwwroot/js/nav-state.js`). **Step 4 — run tests, expect PASS:** `dotnet test ZB.MOM.WW.Theme/` **Step 5 — commit:** `git commit -am "feat(theme): ThemeScripts + localStorage nav-state enhancer"` --- ### Task 0.3: Version bump 0.2.0 + full suite **Classification:** small **Estimated implement time:** ~2 min **Parallelizable with:** none (depends on 0.1, 0.2) **Files:** - Modify: `ZB.MOM.WW.Theme/Directory.Build.props:7` **Steps:** 1. Change `0.1.0` → `0.2.0`. 2. Run `cd ZB.MOM.WW.Theme && dotnet build -c Release` — expect **0 warnings** (TreatWarningsAsErrors). 3. Run `dotnet test` — expect all green (38 existing + the new persistence/ThemeScripts tests). 4. Commit: `git commit -am "chore(theme): bump 0.1.0 -> 0.2.0 (nav persistence + ThemeScripts)"` --- ### Task 0.4: Publish 0.2.0 to Gitea feed **Classification:** small **Estimated implement time:** ~2 min (blocks on user-supplied token) **Parallelizable with:** none (depends on 0.3) **⚠ Requires the user's `GITEA_NUGET_KEY`** (Gitea token with `package:write`). It is not persisted — ask the user to export it (or run the push command themselves via `! …`). Do not invent or store it. **Steps:** 1. Confirm 404 pre-state: `curl -s -o /dev/null -w "%{http_code}\n" https://gitea.dohertylan.com/api/packages/dohertj2/nuget/registration/zb.mom.ww.theme/index.json` (expect `404`). 2. Publish: ```bash cd ZB.MOM.WW.Theme export GITEA_NUGET_SOURCE="https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json" export GITEA_NUGET_KEY="" ./build/push.sh ``` 3. Verify published: re-run the curl — expect `200`; confirm version `0.2.0` is listed. 4. No commit needed (artifacts are gitignored). Record the publish in the task log. --- ## Phase 1 — OtOpcUa AdminUI cutover (repo `~/Desktop/OtOpcUa`, branch `feat/adopt-zb-theme`) > Blocked by Task 0.4. Lowest risk: already side-rail with the kit's exact CSS classes. > **First:** `cd ~/Desktop/OtOpcUa && git checkout -b feat/adopt-zb-theme`. ### Task 1.1: NuGet wiring + usings **Classification:** small **Estimated implement time:** ~3 min **Parallelizable with:** none (gates 1.2–1.5) **Files:** - Modify: `Directory.Packages.props` (repo root) — add `` - Modify: `NuGet.config` (repo root) — under `` add `` - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj` — add `` (versionless; central PM) - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/_Imports.razor` — add `@using ZB.MOM.WW.Theme` **Verify:** `dotnet restore src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj` resolves `ZB.MOM.WW.Theme 0.2.0` from the Gitea feed. Commit. ### Task 1.2: App.razor — ThemeHead + ThemeScripts **Classification:** small **Estimated implement time:** ~2 min **Parallelizable with:** Task 1.3, 1.4, 1.5 **Files:** - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/App.razor` **Edits:** Replace line `…/css/theme.css` `` with `` (keep the Bootstrap `` *above* it and the `…/css/site.css` `` *below* it). Replace `` with ``. Keep the bootstrap bundle + `blazor.web.js` scripts. Commit. ### Task 1.3: Migrate app-only CSS, delete theme.css + fonts + nav-state.js **Classification:** standard **Estimated implement time:** ~4 min **Parallelizable with:** Task 1.2, 1.4, 1.5 **Files:** - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/wwwroot/css/site.css` - Delete: `wwwroot/css/theme.css`, `wwwroot/fonts/ibm-plex-*.woff2` (×3), `wwwroot/js/nav-state.js` **Steps:** Diff `wwwroot/css/theme.css` against the kit's `theme.css`+`layout.css`. Any rule present in the app copy but NOT the kit (notably **`.chip-alert`, `.chip-caution`**, and any app-only tweak) → append to `site.css` under a clearly-commented "App-specific status variants (not in ZB.MOM.WW.Theme)" block. Then delete the four asset files. Keep `wwwroot/js/monaco-loader.js`. Commit. ### Task 1.4: MainLayout → ThemeShell + kit nav **Classification:** high-risk **Estimated implement time:** ~5 min **Parallelizable with:** Task 1.2, 1.3, 1.5 **Files:** - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/MainLayout.razor` - Delete: `Components/Layout/NavSidebar.razor`, `Components/Layout/NavSection.razor` **Context:** Replaces the interactive `NavSidebar` island + bespoke `NavSection` with the kit's static `` + `NavRailSection`/`NavRailItem` (persistence now comes from `ThemeScripts`). All sections default `Expanded=true`; the URL-based auto-expand behavior is intentionally dropped (D2/D3 — localStorage persistence replaces it). Reproduce the 3 sections / 17 links / footer exactly. **Target `MainLayout.razor`:** ```razor @inherits LayoutComponentBase
Session
@context.User.Identity?.Name
@string.Join(", ", context.User.Claims.Where(c => c.Type.EndsWith("/role")).Select(c => c.Value))
Session
Sign in
@Body
``` **Note:** confirm `ThemeShell` exposes `Nav`/`RailFooter`/`ChildContent` slots and that the hamburger/collapse behavior comes from the kit's `layout.css` (Bootstrap collapse JS already loaded). If the kit shell wraps the rail in its own collapse, drop the app's old hamburger markup (now in the shell). Build the AdminUI project; verify it compiles. Commit. ### Task 1.5: Delete dead StatusBadge + Login → LoginCard **Classification:** standard **Estimated implement time:** ~3 min **Parallelizable with:** Task 1.2, 1.3, 1.4 **Files:** - Delete: `Components/Shared/StatusBadge.razor` (verified unused — confirm with a repo grep for `