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.
11 KiB
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:
components/ui-theme/— the normalization docs (spec / design-tokens / shared-contract / per-project current-state / GAPS).ZB.MOM.WW.Theme/— the built RCL, living atscadaproj/ZB.MOM.WW.Theme/(its own folder in this repo, exactly likeZB.MOM.WW.Auth/).
The RCL ships:
- Static web assets — the canonical
theme.css+ the IBM Plex.woff2files, served at_content/ZB.MOM.WW.Theme/css/theme.cssand_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 thesrc: 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). RendersBrandBarin the rail header + theNavslot + a<main>body region.BrandBar— logo + product name; standalone, used internally byThemeLayout.NavRailItem— one rail link:string Href,string Text,RenderFragment? Icon,NavLinkMatch Match. Active state via BlazorNavLink. Apps fill theNavslot with these.
Status (highest-value shared widget)
StatusPill—StatusState State(Ok | Warn | Bad | Idle | Info) +ChildContentlabel. Maps state → the--ok / --warn / --bad / --idletokens.enum StatusStatepublic.
Auth surface
LoginCard— centered branded card:string Product,RenderFragment? Logo,EventCallback<LoginSubmit> OnSubmit,string? Error,bool Busy. UI-only — raisesOnSubmit; no auth logic (the app wires it toZB.MOM.WW.Auth).readonly record struct LoginSubmit(string Username, string Password).
Common controls (thin themed wrappers over Bootstrap 5)
TechButton—ButtonVariant Variant(Primary | Secondary | Danger | Ghost),bool Busy,ChildContent; passes throughtype/onclick.TechCard—string? 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:
StatusPillmaps eachStatusState→ the right token class.ThemeLayoutemits the--accentoverride whenAccentset; rendersProduct,Logo,Nav, and body slots.LoginCardinvokesOnSubmitwith the enteredLoginSubmit; honorsBusy/Error.TechButton/TechCard/TechFieldrender 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.mdas 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.