From 7e11f9aac8c284a932e92dc03ee948fc655bc03f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 3 Jun 2026 02:50:31 -0400 Subject: [PATCH] docs(ui-theme): implementation plan + task graph (26 tasks, Phases 0-4) --- docs/plans/2026-06-03-ui-theme-adoption.md | 643 ++++++++++++++++++ ...2026-06-03-ui-theme-adoption.md.tasks.json | 32 + 2 files changed, 675 insertions(+) create mode 100644 docs/plans/2026-06-03-ui-theme-adoption.md create mode 100644 docs/plans/2026-06-03-ui-theme-adoption.md.tasks.json diff --git a/docs/plans/2026-06-03-ui-theme-adoption.md b/docs/plans/2026-06-03-ui-theme-adoption.md new file mode 100644 index 0000000..278f28e --- /dev/null +++ b/docs/plans/2026-06-03-ui-theme-adoption.md @@ -0,0 +1,643 @@ +# 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 `