From f9d570c3237db7fb970f0c7262a7741a727bfc35 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 1 Jun 2026 04:29:58 -0400 Subject: [PATCH] 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-ui-theme-component-design.md | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 docs/plans/2026-06-01-ui-theme-component-design.md diff --git a/docs/plans/2026-06-01-ui-theme-component-design.md b/docs/plans/2026-06-01-ui-theme-component-design.md new file mode 100644 index 0000000..52adbf2 --- /dev/null +++ b/docs/plans/2026-06-01-ui-theme-component-design.md @@ -0,0 +1,203 @@ +# 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 +`
`, 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** +- `` — dropped in `` (App.razor); emits the `` 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 `
` 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 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 ``/`