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

11 KiB
Raw Blame History

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-spikeT41.
  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.