11 KiB
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-platformofforigin/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
TemplateEditpage-embedded modals to the host. - Theming the external
ZB.MOM.WW.Themeside-rail shell if the spike shows it does not honordata-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:
- Spike first (
T34-spike, small): verify whether the externalZB.MOM.WW.Themeshell (<ThemeShell Product="ScadaBridge" Accent="#2f5fd0">atMainLayout.razor:6) honorsdata-bs-theme— does the side-rail go dark? Decide wiring before committing toggle work. - Token layer (
T34a): introduce an app-level CSS-variable layer inCentralUI/wwwroot/css/site.css(semantic tokens — surface, border, backdrop, etc. — mapped onto Bootstrap dark vars), tokenize the modal-host backdrop, and auditbg-light/white/darkutility usage so content respects the theme. - Dark toggle (
T34b, high-risk): a sun/moon control that setsdata-bs-themeon<html>, persists the choice inlocalStorage, 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 theDeployments.razoroffset viaPagerWindow.csif 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/confirmToastNotification.razorannounces (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.
- Wave 1 (foundation + independent kickoff):
T33a(modal host) ∥T34-spike∥T41. - Wave 2 (build on host / spike):
T33b(migrate dialogs) ∥T34a(token layer + backdrop tokenization) ∥T35a(OffsetPager) ∥T35b(KeysetPager) ∥T35c(DateTimeRangeFilter). - Wave 3 (theme + a11y):
T34b(dark toggle, gated by spike) ∥T36a(aria audit + toast verify). - 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 (
ShowAsyncround-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-liveassertion. - 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/readysmoke, full Playwright run.
Key risks
- External
ZB.MOM.WW.Themedark support (biggest unknown). The package owns the side-rail shell. If it ignoresdata-bs-theme, the toggle darkens Bootstrap content but leaves the rail light — a broken half-dark UI. Mitigation:T34-spikeruns 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. - 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.
- 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.
- 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
KpiTrendChartcustom-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/.csedits follow the surrounding Blazor + Bootstrap idiom; no third-party component frameworks; no new NuGet packages.