f9d570c323
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.
204 lines
11 KiB
Markdown
204 lines
11 KiB
Markdown
# 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.
|