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

204 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)**
- `StatusPill``StatusState 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)**
- `TechButton``ButtonVariant Variant` (`Primary | Secondary | Danger | Ghost`),
`bool Busy`, `ChildContent`; passes through `type`/`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**:
- `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.