Files
ScadaBridge/docs/plans/2026-06-18-m10-uiux-platform-design.md
T

101 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M10 — UI/UX Platform Design (T33T36, T41)
> Final milestone of the system-completion roadmap (`docs/plans/2026-06-15-stillpending-completion-design.md`).
> Branch: `worktree-m10-uiux-platform` off `origin/main` @ `ba335519`.
## Goal
Consolidate the Central UI's cross-cutting presentation concerns into a small set of reusable primitives — a custom-content modal host, a CSS-variable token layer with a dark-mode toggle, extracted pagination/filter components — plus a bounded accessibility pass and Playwright coverage for the alarm-override trigger-config UI. No new functional surfaces; this is platform hardening of what already ships.
## Scope (locked)
| Task | What ships |
|---|---|
| **T33** | Custom-content modal host: extend the existing `DialogHost`/`DialogService` with `ShowAsync<TResult>(title, RenderFragment, …)`, **focus trap + focus restoration + a tokenized backdrop built in**. Migrate the simple reusable dialogs (`MoveFolder`, `MoveDataConnection`, `MoveTemplate`, `RenameFolder`, `ComposeInto`) onto it. Leave the 4 complex `TemplateEdit` page-embedded modals as-is. |
| **T34** | Full dark-mode toggle (sun/moon control) binding `data-bs-theme` on `<html>` + `localStorage` persistence + SSR hydration; an app-level CSS-variable **token layer**; tokenize the ~17 hard-coded `rgba(0,0,0,…)` backdrops (now centralized in the modal host) + audit `bg-light/white/dark` utility usage. **Gated by a verification spike** on the external `ZB.MOM.WW.Theme` shell. |
| **T35** | Extract three targeted components — `OffsetPager`, `KeysetPager`, `DateTimeRangeFilter` — and adopt them into the pages that already share each pattern. **Not** a unified offset+keyset framework. |
| **T36** | Bounded accessibility pass: aria-label audit/fix for icon buttons, status badges, and spinners; modal focus trap + restoration (delivered **inside** the T33 host); toast `aria-live` verification. No TreeView arrow-key navigation (R7 stays deferred). |
| **T41** | Extend the existing `AlarmOverride_SetPriority_ThenClear_RoundTrips` Playwright test with trigger-config scenarios: HiLo setpoint edit (merge), non-HiLo whole-replace, a validation error, modal cancel, clear-from-modal. |
### Explicitly out of scope (deferred / logged)
- A unified offset+keyset pagination framework — blocked by the offset-vs-keyset total-count mismatch; the three extracted components stay separate.
- TreeView arrow-key navigation (R7).
- Migrating the complex `TemplateEdit` page-embedded modals to the host.
- Theming the external `ZB.MOM.WW.Theme` side-rail shell if the spike shows it does not honor `data-bs-theme` (becomes a coordination follow-up, not M10 work).
## Architecture
### T33 — Custom-content modal host
The existing `DialogHost.razor` / `DialogService` / `IDialogService` already provide `ConfirmAsync` / `PromptAsync` and are DI-registered (`ServiceCollectionExtensions.cs:37`) and rendered once in `MainLayout`. M10 **extends, not replaces**: add a generic `ShowAsync<TResult>(string title, RenderFragment body, …)` overload that renders arbitrary child content inside the same host chrome and resolves a typed result when the dialog closes. The host owns:
- the backdrop (a single tokenized element — replaces ~17 per-dialog `rgba(0,0,0,…)` backdrops),
- **focus trap** (Tab/Shift-Tab cycle within the modal) + **focus restoration** (return focus to the trigger element on close),
- Escape-to-cancel and click-outside-to-cancel (consistent with current behavior).
Migrated dialogs (`MoveFolder`, `MoveDataConnection`, `MoveTemplate`, `RenameFolder`, `ComposeInto`) drop their own backdrop/markup and become `RenderFragment` bodies passed to `ShowAsync<T>`. Each migration must preserve the dialog's existing observable behavior and keep its current bUnit fixture (or the page's) green.
### T34 — Token layer + dark mode (spike-gated)
Bootstrap 5.3.3 (bundled at `Host/wwwroot/lib/bootstrap`) natively supports `data-bs-theme="dark"`. The plan:
1. **Spike first** (`T34-spike`, small): verify whether the external `ZB.MOM.WW.Theme` shell (`<ThemeShell Product="ScadaBridge" Accent="#2f5fd0">` at `MainLayout.razor:6`) honors `data-bs-theme` — does the side-rail go dark? Decide wiring before committing toggle work.
2. **Token layer** (`T34a`): introduce an app-level CSS-variable layer in `CentralUI/wwwroot/css/site.css` (semantic tokens — surface, border, backdrop, etc. — mapped onto Bootstrap dark vars), tokenize the modal-host backdrop, and audit `bg-light/white/dark` utility usage so content respects the theme.
3. **Dark toggle** (`T34b`, high-risk): a sun/moon control that sets `data-bs-theme` on `<html>`, persists the choice in `localStorage`, and hydrates the correct theme on first paint (SSR — avoid a light-flash). Blazor Server: the toggle is a small JS-interop + a persisted preference.
**Risk gate:** `T34a`/`T34b` do not start until `T34-spike` reports whether the shell can go dark. If it cannot, we ship tokens-only and log the shell-theming coordination as a follow-up — surfaced to the user before proceeding.
### T35 — Targeted pager/filter extractions
Three independent extractions, each adopted only by pages that already use that exact pattern:
- **`OffsetPager`** — page-number/offset paging → `NotificationReport.razor`, `ConfigurationAuditLog.razor` (and the `Deployments.razor` offset via `PagerWindow.cs` if it folds in cleanly).
- **`KeysetPager`** — cursor/keyset paging with a cursor stack → `SiteCallsReport.razor`, `Components/Audit/AuditResultsGrid.razor`.
- **`DateTimeRangeFilter`** — the existing date-range filter pattern (CentralUI-027, ~8 pages) extracted into one component with UTC conversion centralized.
No shared base abstraction across offset and keyset — they have genuinely different contracts (offset knows total count; keyset does not). Pages touched by **both** a pager and the date-range filter (NotificationReport, SiteCalls, ConfigAudit) get serialized edits in the plan.
### T36 — Bounded accessibility pass
- **aria-label audit/fix**: icon-only buttons (edit/clear/move/reorder), status badges, and spinners get accessible names. Bounded to high-traffic operator + config pages.
- **focus trap + restoration**: delivered *in* the T33 host (one implementation, not per-dialog).
- **toast `aria-live`**: verify/confirm `ToastNotification.razor` announces (`aria-live="polite"` already at line 14) and that dynamically-added toasts are announced.
### T41 — Alarm-override Playwright extension
Extend the existing test (`Deployment/InstanceConfigureTests.cs:192`) and/or add sibling specs against the docker cluster. The `InstanceConfigureFixture` already provisions a HiLo alarm (`AlarmName="HiHi"`, hi:80, hiHi:95); all data-test hooks exist (`alarm-override-row-{name}`, `alarm-edit-btn`, `alarm-priority-input`, `alarm-save-override`, `alarm-override-badge`, `alarm-clear-btn`). Scenarios: HiLo setpoint edit (merge semantics), non-HiLo whole-replace, a validation error, modal cancel (no change), clear-from-modal.
## Build order & waves
The modal host is foundational — it's the single home for T34's tokenized backdrop and T36's focus trap — so it goes first; everything else is independent and fans out.
1. **Wave 1 (foundation + independent kickoff):** `T33a` (modal host) ∥ `T34-spike``T41`.
2. **Wave 2 (build on host / spike):** `T33b` (migrate dialogs) ∥ `T34a` (token layer + backdrop tokenization) ∥ `T35a` (OffsetPager) ∥ `T35b` (KeysetPager) ∥ `T35c` (DateTimeRangeFilter).
3. **Wave 3 (theme + a11y):** `T34b` (dark toggle, gated by spike) ∥ `T36a` (aria audit + toast verify).
4. **Wave 4:** `INT` — full-solution build, docker rebuild, full Playwright run (incl. T41), a11y smoke, docs sync, whole-branch integration review.
### Classifications
| Task | Class |
|---|---|
| T33a (modal host — shared UI infra, focus trap) | **high-risk** |
| T33b (migrate dialogs) | standard |
| T34-spike (Theme dark verification) | small |
| T34a (token layer + backdrop tokenize + bg-* audit) | standard |
| T34b (dark toggle + persistence + SSR hydration) | **high-risk** |
| T35a (OffsetPager) | standard |
| T35b (KeysetPager) | standard |
| T35c (DateTimeRangeFilter) | standard |
| T36a (aria audit + toast verify) | standard |
| T41 (alarm-override Playwright extension) | standard |
| INT | **high-risk** |
## Testing
- **bUnit**: modal host (`ShowAsync` round-trip, focus trap, backdrop token, Escape/click-outside cancel); each pager (offset paging math, keyset cursor stack push/pop, date-range UTC conversion); each migrated dialog keeps its existing fixture green.
- **Component a11y**: aria-name assertions in the relevant component tests; toast `aria-live` assertion.
- **Dark toggle**: persistence + hydration unit/interop test.
- **T41**: live Playwright against the docker cluster (`localhost:9000` / `ws://localhost:3000`).
- **INT**: full solution build (`dotnet build ZB.MOM.WW.ScadaBridge.slnx`, `TreatWarningsAsErrors`), EF model-drift check, every touched test project unfiltered, docker rebuild (`bash docker/deploy.sh`) + `/health/ready` smoke, full Playwright run.
## Key risks
1. **External `ZB.MOM.WW.Theme` dark support (biggest unknown).** The package owns the side-rail shell. If it ignores `data-bs-theme`, the toggle darkens Bootstrap content but leaves the rail light — a broken half-dark UI. **Mitigation:** `T34-spike` runs first and gates T34a/T34b; if the shell can't go dark, surface it and decide (tokens-only ship, or coordinate a Theme-package change) before building the toggle.
2. **Shared report-page files** (NotificationReport / SiteCalls / ConfigAudit are each touched by both a pager extraction and the date-range filter) → serialize those edits in the plan to avoid concurrent-edit collisions.
3. **Modal host is shared infra.** Migrating dialogs onto it must preserve each dialog's behavior and keep its existing tests green — a regression here hits multiple pages. The whole-branch integration reviewer must re-run every migrated dialog's fixture.
4. **No new NuGet packages** (central package management) and **no third-party Blazor component frameworks** — the dark toggle, token layer, and pagers are all custom (consistent with the existing custom-component rule). The `KpiTrendChart` custom-SVG precedent confirms this is the house style.
## Constraints (project standing rules)
- Work in this dedicated worktree, never the primary checkout; pathspec commits only (`git commit -m "msg" -- <paths>`); ≤23 concurrent committers + post-wave HEAD-presence checks.
- Targeted tests/builds per task; full-solution build + docker rebuild + Playwright only at INT.
- `.razor`/`.cs` edits follow the surrounding Blazor + Bootstrap idiom; no third-party component frameworks; no new NuGet packages.