Files
scadaproj/docs/plans/2026-06-01-ui-theme-component-design.md
T
Joseph Doherty f9d570c323 docs: add UI-theme component design
Brainstormed design for normalizing UI theming across the 3 sister apps
into a single .NET 10 RCL (ZB.MOM.WW.Theme): canonical side-rail shell +
Technical-Light tokens/fonts as static assets + StatusPill/LoginCard/
TechButton-Card-Field, with per-app name/accent/logo. Mirrors the auth
component's path-to-shared-code treatment; app adoption tracked as
follow-on.
2026-06-01 04:29:58 -04:00

11 KiB
Raw Blame History

UI Theme Component — Design

Date: 2026-06-01 Status: Approved (brainstorming) → ready for writing-plans Goal: Normalize UI theming across the three sister apps and realize it as a single shared .NET 10 Razor Class Library, ZB.MOM.WW.Theme — mirroring the auth component's "path to shared code" treatment.


1. Motivation (code-verified, 2026-06-01)

All three sister apps have Blazor SSR + Bootstrap 5 UIs (four surfaces total):

App UI surface(s) Nav layout today
OtOpcUa ZB.MOM.WW.OtOpcUa.AdminUI side rail (NavSidebar.razor)
MxAccessGateway ZB.MOM.WW.MxGateway.Server Dashboard top nav bar
ScadaBridge ZB.MOM.WW.ScadaBridge.Host + .CentralUI (RCL) own MainLayout + NavMenu

Each ships a hand-copied theme.css — the "Technical-Light" design system (379 lines: IBM Plex @font-face, design tokens :root { --paper, --card, --ink, --ink-soft, --rule, --accent, --ok, --warn, --bad, --idle }, status palette, typography). The three copies are byte-for-byte identical except for three lines — the font src: URL prefix (fonts/ vs /fonts/ vs ../fonts/), a per-app deployment path, not a design difference. IBM Plex .woff2 fonts are vendored separately into each app's wwwroot/fonts/.

This is the textbook drift situation: a shared design system maintained as three copy-pasted files that have already begun to diverge. The split between shared and per-project is unusually clean — see §6.

Verified paths:

  • OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/wwwroot/css/theme.css (+ site.css, fonts/)
  • MxAccessGateway/src/ZB.MOM.WW.MxGateway.Server/wwwroot/css/theme.css (+ site.css, fonts/)
  • ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/theme.css (+ site.css, fonts/)

2. Decisions (from brainstorming)

Decision Choice
Depth/goal Full shared UI kit — beyond tokens: extract layout shell + components into the RCL
Layout model One canonical layout — a single side-rail shell; top-bar / menu apps migrate onto it
Branding Name + accent + logo per app — uniform chassis, per-app identity via parameters
Kit contents Canonical shell + tokens + fonts and Status indicators and Login card and Common form controls
Packaging Single RCL ZB.MOM.WW.Theme (Approach A) — one package, one version

Packaging alternatives considered and rejected: (B) split CSS/fonts vs components — YAGNI, no tokens-only consumer exists, and splitting later is non-breaking; (C) plain content package, no components — contradicts the "full kit" decision.

3. Architecture

Two deliverables, same shape as the auth component:

  1. components/ui-theme/ — the normalization docs (spec / design-tokens / shared-contract / per-project current-state / GAPS).
  2. ZB.MOM.WW.Theme/ — the built RCL, living at scadaproj/ZB.MOM.WW.Theme/ (its own folder in this repo, exactly like ZB.MOM.WW.Auth/).

The RCL ships:

  • Static web assets — the canonical theme.css + the IBM Plex .woff2 files, served at _content/ZB.MOM.WW.Theme/css/theme.css and _content/ZB.MOM.WW.Theme/fonts/…. This alone kills the copy-paste drift, and fixes the font-path divergence for free: the RCL's static-asset base path makes the src: url(../fonts/…) reference canonical and identical for every consumer.
  • Razor components — the canonical side-rail shell + the chosen widgets (§4).

Accent/branding mechanism: ThemeLayout renders its root as <div class="app-shell" style="--accent: @Accent">, so a per-app accent overrides the --accent custom property for that subtree. Product name and logo are parameters. Everything else in Technical-Light stays shared and un-overridable.

Adoption is follow-on, per repo (like auth's GAPS #8). Building the RCL is self-contained in scadaproj; apps referencing it, deleting their theme.css/fonts/MainLayout/login card, and migrating top-bar → rail (MxGateway) is tracked in GAPS.md, not done in this repo.

4. Component contract (RCL public API)

Namespace ZB.MOM.WW.Theme. Components are themed purely via CSS custom properties — no hardcoded colors in markup.

Static-asset entry point

  • <ThemeHead /> — dropped in <head> (App.razor); emits the <link> to _content/ZB.MOM.WW.Theme/css/theme.css. One line replaces each app's hand-rolled wiring.

Layout shell (canonical chassis)

  • ThemeLayout : LayoutComponentBase — the one side-rail shell. Params: string Product, string? Accent (overrides --accent), RenderFragment? Logo, RenderFragment Nav (rail nav items), RenderFragment? RailFooter (e.g. user/sign-out), RenderFragment ChildContent (page body). Renders BrandBar in the rail header + the Nav slot + a <main> body region.
  • BrandBar — logo + product name; standalone, used internally by ThemeLayout.
  • NavRailItem — one rail link: string Href, string Text, RenderFragment? Icon, NavLinkMatch Match. Active state via Blazor NavLink. Apps fill the Nav slot with these.

Status (highest-value shared widget)

  • StatusPillStatusState State (Ok | Warn | Bad | Idle | Info) + ChildContent label. Maps state → the --ok / --warn / --bad / --idle tokens. enum StatusState public.

Auth surface

  • LoginCard — centered branded card: string Product, RenderFragment? Logo, EventCallback<LoginSubmit> OnSubmit, string? Error, bool Busy. UI-only — raises OnSubmit; no auth logic (the app wires it to ZB.MOM.WW.Auth). readonly record struct LoginSubmit(string Username, string Password).

Common controls (thin themed wrappers over Bootstrap 5)

  • TechButtonButtonVariant Variant (Primary | Secondary | Danger | Ghost), bool Busy, ChildContent; passes through type/onclick.
  • TechCardstring? Title, RenderFragment? Header, ChildContent, RenderFragment? Footer.
  • TechField — labeled input wrapper: string Label, string? Hint, string? Error, ChildContent (the <input>/<select>).

Deliberately NOT included (YAGNI / stays per-project): data grids, tree views, dropdowns, modals, toasts, page-specific layouts. The kit owns chrome + tokens + the four widgets above — not a general component library.

Project shape: Microsoft.NET.Sdk.Razor, net10.0, FrameworkReference Microsoft.AspNetCore.App; wwwroot/css/theme.css + wwwroot/fonts/*.woff2 as static assets. Tests via bUnit; central package management, matching ZB.MOM.WW.Auth.

5. Normalization folder layout

components/ui-theme/
  README.md                       # overview + per-project status table
  spec/
    SPEC.md                       # the ONE normalized target for UI theming
    DESIGN-TOKENS.md              # canonical token reference (analogous to auth CANONICAL-ROLES.md)
  shared-contract/
    ZB.MOM.WW.Theme.md            # the RCL public API (§4), packaging, consumer matrix
  current-state/
    otopcua/CURRENT-STATE.md      # code-verified theming today + adoption plan
    mxaccessgw/CURRENT-STATE.md
    scadabridge/CURRENT-STATE.md
  GAPS.md                         # divergences vs SPEC + adoption backlog

spec/SPEC.md sections: §1 design tokens (Technical-Light palette + type scale; --accent is the one per-app override) · §2 typography (IBM Plex Sans 400/600 + Mono 500, vendored woff2, canonical relative font path) · §3 canonical side-rail layout · §4 component contract (the §4 surface) · §5 delivery (RCL static assets, <ThemeHead/>) · §6 shared vs per-project (see below) · §7 acceptance (what "adopted" means per app).

spec/DESIGN-TOKENS.md: enumerates every token (name, value, role) as the lookup reference, the way CANONICAL-ROLES.md enumerates roles. The canonical 379-line theme.css is the source; this doc is its human-readable index.

shared-contract/ZB.MOM.WW.Theme.md: the package API from §4 + the consumer matrix — all three apps consume the single RCL (OtOpcUa AdminUI, MxGateway Server Dashboard, ScadaBridge Host + CentralUI); no optional packages, unlike auth.

Register the component in components/README.md (new row: UI Theme — layout / tokens / components → ZB.MOM.WW.Theme RCL) and add it to the root CLAUDE.md / README.md component tables.

6. Shared vs per-project

Shared (extracted into the RCL): design tokens, IBM Plex fonts, the canonical side-rail shell, and the four widgets (StatusPill, LoginCard, TechButton/Card/Field).

Per-project (NOT extracted): each app's site.css residual page layout, its page/route content, and app-specific scoped .razor.css (e.g. ScadaBridge's MultiSelectDropdown, TreeView, Audit/*). The kit owns the chrome and tokens, not the app's domain screens.

7. Current-state + adoption (per project)

Project Surface(s) Today Adoption = delete / change / keep
OtOpcUa AdminUI (side rail) theme.css + IBM Plex fonts; MainLayout + NavSidebar; login card in site.css Lowest effort — already a rail. Delete theme.css/fonts, render in ThemeLayout, swap login card for LoginCard. Keep site.css page bits.
MxAccessGateway Server Dashboard (top bar) identical theme.css/fonts; top-nav MainLayout Highest effort/risk — top-bar → side-rail migration (the "one canonical layout" cost lands here).
ScadaBridge Host + CentralUI (own menu) identical theme.css/fonts; MainLayout + NavMenu; scoped .razor.css Migrate shell to ThemeLayout; keep scoped component CSS — stays per-project.

GAPS.md — divergences + prioritized adoption backlog:

  • Divergences: tokens identical today but copy-pasted (drift started on font paths); layouts differ (rail / top-bar / menu); fonts vendored 3×.
  • Backlog (priority · effort · risk): (1) build the RCL [scadaproj]; (2) adopt in OtOpcUa [low risk]; (3) adopt in ScadaBridge [med]; (4) migrate MxGateway top-bar → rail [high risk — UX change, flagged explicitly]. Same "adoption is per-repo follow-on" framing as auth's GAPS #8.

8. Testing

In the RCL, via bUnit:

  • StatusPill maps each StatusState → the right token class.
  • ThemeLayout emits the --accent override when Accent set; renders Product, Logo, Nav, and body slots.
  • LoginCard invokes OnSubmit with the entered LoginSubmit; honors Busy/Error.
  • TechButton / TechCard / TechField render variants/slots correctly.
  • A packaging test asserting theme.css + the woff2 files ship as _content/ZB.MOM.WW.Theme/... static assets (the anti-drift guarantee).
  • No visual/snapshot regression tests — YAGNI for this pass.

Build/test/pack (from ZB.MOM.WW.Theme/): dotnet build -c Release · dotnet test · dotnet pack -c Release -o ./artifacts → 1 nupkg.

9. Out of scope (this pass)

  • App-side adoption (referencing the RCL, deleting copies, the MxGateway layout migration) — tracked in GAPS.md as follow-on, per repo.
  • A general component library beyond the four widgets.
  • Dark mode / theme switching (Technical-Light is the only theme today).
  • Visual regression testing.