From d8ba99d19d09d79fee0890f86057b0016fc23666 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 19:15:54 -0400 Subject: [PATCH] =?UTF-8?q?docs(m10):=20UI/UX=20platform=20implementation?= =?UTF-8?q?=20plan=20=E2=80=94=2017=20tasks,=205=20waves=20(T33-T36,=20T41?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/2026-06-18-m10-uiux-platform.md | 462 ++++++++++++++++++ ...2026-06-18-m10-uiux-platform.md.tasks.json | 23 + 2 files changed, 485 insertions(+) create mode 100644 docs/plans/2026-06-18-m10-uiux-platform.md create mode 100644 docs/plans/2026-06-18-m10-uiux-platform.md.tasks.json diff --git a/docs/plans/2026-06-18-m10-uiux-platform.md b/docs/plans/2026-06-18-m10-uiux-platform.md new file mode 100644 index 00000000..c96d3cdd --- /dev/null +++ b/docs/plans/2026-06-18-m10-uiux-platform.md @@ -0,0 +1,462 @@ +# M10 — UI/UX Platform Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Consolidate Central UI cross-cutting presentation into reusable primitives — a custom-content modal host, a dark-mode token layer, extracted pager/filter components — plus a bounded a11y pass and Playwright coverage for the alarm-override trigger-config UI. No new functional surfaces. + +**Architecture:** Extend the existing `DialogHost`/`DialogService` with a generic `ShowAsync` overload that owns focus-trap + focus-restoration + a single tokenized backdrop, then migrate the 5 simple ad-hoc dialogs onto it. Add a CSS-variable dark token layer in `site.css` (loads after the compiled `ZB.MOM.WW.Theme` `theme.css`, overriding its light-only `:root` tokens under `[data-bs-theme="dark"]`) plus a `localStorage`-backed toggle. Extract three **purely presentational** components (`OffsetPager`, `KeysetPager`, `DateTimeRangeFilter`) — navigation/cursor/UTC logic stays page-side — and adopt them per-page so each shared file is owned by exactly one task. + +**Tech Stack:** Blazor Server (.NET 10, `TreatWarningsAsErrors`), Bootstrap 5.3.3 (`data-bs-theme` aware), external `ZB.MOM.WW.Theme` 0.3.1 (compiled NuGet — no source), bUnit + xUnit, Microsoft.Playwright C#. No new NuGet packages; no third-party Blazor component frameworks. + +**Base:** branch `worktree-m10-uiux-platform` off `origin/main` @ `ba335519`. Design: `docs/plans/2026-06-18-m10-uiux-platform-design.md` (committed `1a23b902`). + +--- + +## Execution rules (every task) + +- **Worktree:** you are already in `.claude/worktrees/m10-uiux-platform`. Implementers do **NOT** create worktrees. +- **Commits:** pathspec form only — `git commit -m "msg" -- ` (`-m` BEFORE `--`). Never `git add -A`/`-a`/`.`. For new files, `git add ` then pathspec commit. Retry on `index.lock`. +- **Concurrency:** ≤2–3 concurrent committers per wave; post-wave HEAD-presence check (`git log --oneline` shows every wave commit; recover orphans via cherry-pick). +- **Builds/tests:** targeted per task (build only the touched project; run the named filtered test). Full-solution build + docker rebuild + Playwright + live smoke happen **only in the INT task**. +- **File ownership:** the `Files:` block is the contract. Each shared file is owned by exactly one task per wave. If you need a file not listed, STOP — it's a plan defect; surface it. + +## Wave / dependency map + +| Wave | Tasks (native IDs assigned at creation) | +|---|---| +| 1 | T33a (modal host) ∥ T34-spike ∥ T41 | +| 2 | T33b (migrate dialogs, ⟵T33a) ∥ T34a (token layer, ⟵spike) ∥ T35a (OffsetPager) ∥ T35b (KeysetPager) ∥ T35c (DateTimeRangeFilter) | +| 3 | T35d (NotificationReport ⟵T35a,T35c,T34a) ∥ T35e (ConfigAudit ⟵T35a,T35c) ∥ T35f (SiteCalls ⟵T35b,T35c,T34a) ∥ T35g (AuditGrid+FilterBar ⟵T35b,T35c) ∥ T35h (EventLogs ⟵T35c) ∥ T34c (standalone backdrops ⟵T34a) | +| 4 | T34b (dark toggle ⟵spike,T34a) ∥ T36a (aria pass) | +| 5 | INT | + +--- + +## Wave 1 + +### Task T33a: Modal host — `ShowAsync` + focus trap + focus restoration + tokenized backdrop + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** T34-spike, T41 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/IDialogService.cs` +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/DialogService.cs` +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/DialogHost.razor` +- Create: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/DialogHostShowAsyncTests.cs` + +**Context:** The host today (read it) renders Confirm/Prompt only, with a Bootstrap `.modal-backdrop`, once-per-dialog focus, and Escape-to-cancel. We add a third dialog kind — **custom content** — that renders an arbitrary `RenderFragment` and resolves a typed result the body sets via a callback. This becomes the single home for the migrated dialogs (T33b), the tokenized backdrop (T34a), and the focus trap (T36/this task). + +**Design contract:** +- `IDialogService` gains: + ```csharp + // Shows arbitrary content. The body fragment receives a controller it can call to + // close the dialog with a typed result (or null = cancel). Resolves when closed. + Task ShowAsync(string title, RenderFragment> body, string? size = null); + ``` + where `DialogContext` exposes `void Close(TResult result)` and `void Cancel()`. +- `DialogService`: add `DialogKind.Custom`; store the `RenderFragment` and the typed-close delegate on `DialogState` (widen the record additively — keep existing positional params, add optional `Content` + `Size` + a non-generic `Func`/boxed TCS already present). Reuse the existing single-dialog `EnsureNoActiveDialog` guard and the boxed `_tcs`/`Resolve` plumbing (custom resolves with the boxed `TResult?`). +- `DialogHost.razor`: + - render branch for `DialogKind.Custom` → `@state.Content(ctx)` inside `.modal-body`, with the footer suppressed (the custom body supplies its own buttons), and `modal-dialog` size class from `state.Size` (e.g. `modal-lg`). + - **Focus trap:** add an `@onkeydown` handler that, on `Tab`/`Shift+Tab`, keeps focus within `_modalRef` (query focusable elements via a tiny JS interop helper `sbDialog.focusTrap(modalEl, shiftKey)` OR a pure-Blazor first/last sentinel approach — prefer the JS helper added to a new `wwwroot/js/dialog.js`, registered in `App.razor` head as ``). Keep Escape-cancel. + - **Focus restoration:** before opening, capture `document.activeElement` (JS `sbDialog.captureFocus()` returns an opaque handle or stashes it); on close, `sbDialog.restoreFocus()`. Implement in `dialog.js`; call from `OnAfterRenderAsync` open/close branches. + - **Tokenized backdrop:** change the backdrop `