# M10 — UI/UX Platform Design (T33–T36, 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>`); ≤2–3 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.
