From 95975d0754320bd925590bddfe19dcd921f73fb4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 1 Jun 2026 05:11:43 -0400 Subject: [PATCH] docs(ui-theme): spec, design tokens, shared contract --- components/ui-theme/README.md | 45 ++++ .../shared-contract/ZB.MOM.WW.Theme.md | 244 ++++++++++++++++++ components/ui-theme/spec/DESIGN-TOKENS.md | 123 +++++++++ components/ui-theme/spec/SPEC.md | 214 +++++++++++++++ 4 files changed, 626 insertions(+) create mode 100644 components/ui-theme/README.md create mode 100644 components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md create mode 100644 components/ui-theme/spec/DESIGN-TOKENS.md create mode 100644 components/ui-theme/spec/SPEC.md diff --git a/components/ui-theme/README.md b/components/ui-theme/README.md new file mode 100644 index 0000000..c257220 --- /dev/null +++ b/components/ui-theme/README.md @@ -0,0 +1,45 @@ +# UI Theme (layout / tokens / components) + +Second normalized component. **Goal: path to shared code** — converge the three sister +projects onto a common "Technical-Light" design system, realized as the `ZB.MOM.WW.Theme` +Razor Class Library. + +- The one target: [`spec/SPEC.md`](spec/SPEC.md) +- Design tokens reference: [`spec/DESIGN-TOKENS.md`](spec/DESIGN-TOKENS.md) +- The shared library: [`shared-contract/ZB.MOM.WW.Theme.md`](shared-contract/ZB.MOM.WW.Theme.md) +- Divergences + backlog: [`GAPS.md`](GAPS.md) +- Current state, per project: [`current-state/`](current-state/) + +## Why UI theme is a strong candidate + +All three sister apps share a Blazor SSR + Bootstrap 5 UI stack and each ships a +hand-copied **379-line `theme.css`** (the "Technical-Light" design system: IBM Plex +`@font-face`, `:root` design tokens, status palette, typography helpers). **The three +copies are byte-for-byte identical except for three lines** — the `@font-face` `src:` +URL prefix differs per app deployment convention. IBM Plex `.woff2` fonts are likewise +vendored three times into each app's `wwwroot/fonts/`. This is the textbook drift +situation: a shared design system already beginning to diverge, with a latent font-path +bug in one app (OtOpcUa) that goes unnoticed because browsers fall back to system fonts. + +## Status by project + +| Project | Surface | Layout today | Adoption status | +|---|---|---|---| +| **OtOpcUa** | `ZB.MOM.WW.OtOpcUa.AdminUI` | Side rail (`NavSidebar.razor`) + `theme.css` + IBM Plex | Not started | +| **MxAccessGateway** | `ZB.MOM.WW.MxGateway.Server` Dashboard | Sidebar (`nav.sidebar`) + `theme.css` + IBM Plex | Not started | +| **ScadaBridge** | `ZB.MOM.WW.ScadaBridge.Host` + `.CentralUI` (RCL) | Own `MainLayout` + `NavMenu` (`nav.sidebar`) + `theme.css` + IBM Plex | Not started | + +See each project's [`current-state//CURRENT-STATE.md`](current-state/) for the +code-verified detail and its adoption plan. + +## Normalized vs. left per-project + +**Normalized (extracted into the RCL `ZB.MOM.WW.Theme`):** design tokens + IBM Plex +fonts, the canonical side-rail shell (`ThemeShell` + `BrandBar` + `NavRailItem` + +`NavRailSection`), and the four widgets (`StatusPill`, `LoginCard`, `TechButton`, +`TechCard`, `TechField`). One RCL, one package, one version. + +**Left 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. diff --git a/components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md b/components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md new file mode 100644 index 0000000..72fbaa8 --- /dev/null +++ b/components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md @@ -0,0 +1,244 @@ +# Shared library: `ZB.MOM.WW.Theme` + +**Status: Built (`0.1.0`).** The RCL lives at +[`scadaproj/ZB.MOM.WW.Theme/`](../../../ZB.MOM.WW.Theme/) — built and tested. Adoption +by the three apps is follow-on, tracked in [`../GAPS.md`](../GAPS.md). Realizes +[`../spec/SPEC.md`](../spec/SPEC.md). + +--- + +## Package + +One NuGet package — unlike `ZB.MOM.WW.Auth`'s four-package split, there are no +tokens-only or components-only consumers; all three apps consume the full kit. + +| Package | Target | Notes | +|---|---|---| +| `ZB.MOM.WW.Theme` | `net10.0` Razor Class Library | Tokens + fonts + layout CSS + all components | + +Published to the Gitea NuGet feed; `Version 0.1.0`. SemVer — token changes are +breaking (major bump). Build from `scadaproj/ZB.MOM.WW.Theme/`: +```bash +dotnet build -c Release # 0 warnings (TreatWarningsAsErrors) +dotnet test # 32 bUnit tests +./build/pack.sh # → ./artifacts/ZB.MOM.WW.Theme.0.1.0.nupkg +``` + +--- + +## Consumer matrix + +All three apps consume the single RCL. No optional packages. + +| Consumer | Surface | Consumes | +|---|---|---| +| **OtOpcUa** `ZB.MOM.WW.OtOpcUa.AdminUI` | Admin UI (Blazor SSR, side rail) | `ZB.MOM.WW.Theme` | +| **MxAccessGateway** `ZB.MOM.WW.MxGateway.Server` | Dashboard (Blazor SSR) | `ZB.MOM.WW.Theme` | +| **ScadaBridge** `ZB.MOM.WW.ScadaBridge.Host` + `ZB.MOM.WW.ScadaBridge.CentralUI` | Central UI (Blazor SSR) | `ZB.MOM.WW.Theme` | + +--- + +## Static assets + +Served at `_content/ZB.MOM.WW.Theme/…` by ASP.NET's static-web-asset pipeline. + +| Path | Contents | +|---|---| +| `css/theme.css` | Design tokens, typography, Bootstrap 5 overrides (379 lines) | +| `css/layout.css` | Side-rail shell layout, collapsible nav CSS, `StatusPill` variants, `TechCard`/`TechField` helpers | +| `fonts/ibm-plex-sans-400.woff2` | IBM Plex Sans Regular — vendored, no CDN | +| `fonts/ibm-plex-sans-600.woff2` | IBM Plex Sans SemiBold — vendored, no CDN | +| `fonts/ibm-plex-mono-500.woff2` | IBM Plex Mono Medium — vendored, no CDN | + +`theme.css` uses `url('../fonts/ibm-plex-*.woff2')` — the correct relative path from +`css/` to `fonts/` in the static-web-asset tree. + +--- + +## Component API + +Namespace: `ZB.MOM.WW.Theme`. All components live in this flat namespace; one +`@using ZB.MOM.WW.Theme` in `_Imports.razor` covers everything. + +### `ThemeHead` + +Emits `` tags for `theme.css` and `layout.css`. No parameters. + +```razor + +``` + +Place in `App.razor` `` **after** the app's Bootstrap link. + +--- + +### `ThemeShell` + +Canonical side-rail chassis. **Not a `LayoutComponentBase`** — delegated to from the app's +thin `MainLayout`. The `Accent` parameter overrides `--accent` for the shell subtree. + +| Parameter | Type | Required | Default | Notes | +|---|---|---|---|---| +| `Product` | `string` | Yes | — | Product name rendered in `BrandBar` | +| `Accent` | `string?` | No | `null` | Override `--accent` for this app (e.g. `#2f855a`) | +| `Logo` | `RenderFragment?` | No | `null` | Custom logo; replaces default `▐` glyph | +| `Nav` | `RenderFragment?` | No | `null` | Rail nav items (`NavRailSection` / `NavRailItem`) | +| `RailFooter` | `RenderFragment?` | No | `null` | Session block / sign-out at rail bottom | +| `ChildContent` | `RenderFragment?` | No | `null` | Page body (`@Body` from `MainLayout`) | + +**Adoption pattern** — the thin `MainLayout`: + +```razor +@* Components/Layout/MainLayout.razor — replaces the app's existing MainLayout *@ +@inherits LayoutComponentBase + + + + @* AuthorizeView session block / sign-out link *@ + + @Body + +``` + +--- + +### `BrandBar` + +Brand glyph + product name. Rendered inside `ThemeShell`'s rail header; also usable +standalone. + +| Parameter | Type | Required | Notes | +|---|---|---|---| +| `Product` | `string` | Yes | Displayed product name | +| `Logo` | `RenderFragment?` | No | Replaces default `▐` glyph when provided | + +--- + +### `NavRailItem` + +One rail navigation link. Wraps Blazor ``. + +| Parameter | Type | Required | Default | Notes | +|---|---|---|---|---| +| `Href` | `string` | Yes | — | Link target | +| `Text` | `string` | Yes | — | Label text | +| `Icon` | `RenderFragment?` | No | `null` | Optional icon span | +| `Match` | `NavLinkMatch` | No | `Prefix` | Active-class matching behavior | + +--- + +### `NavRailSection` + +Collapsible nav section group using CSS-only `
` — no JavaScript, works in +static Blazor SSR. Apps that need interactive cookie-persisted expand state may keep a +bespoke interactive `NavSection` alongside this. + +| Parameter | Type | Required | Default | Notes | +|---|---|---|---|---| +| `Title` | `string` | Yes | — | Eyebrow label | +| `Expanded` | `bool` | No | `true` | Initial open state | +| `ChildContent` | `RenderFragment?` | No | `null` | `NavRailItem` children | + +--- + +### `StatusPill` + +Inline status chip. Maps `StatusState` to a token-based chip class. + +| Parameter | Type | Required | Notes | +|---|---|---|---| +| `State` | `StatusState` | Yes | `Ok`, `Warn`, `Bad`, `Idle`, `Info` | +| `ChildContent` | `RenderFragment?` | No | Label text | + +```csharp +public enum StatusState { Ok, Warn, Bad, Idle, Info } +``` + +CSS classes emitted: `chip chip-ok` / `chip-warn` / `chip-bad` / `chip-idle` / `chip-info`. + +--- + +### `LoginCard` + +Static form-POST sign-in card. Login **must** use a static form POST — `SignInAsync` must +run before the HTTP response starts; an interactive `EventCallback` fires too late. + +| Parameter | Type | Required | Default | Notes | +|---|---|---|---|---| +| `Product` | `string` | Yes | — | Product name in the card heading | +| `Action` | `string` | No | `/auth/login` | Form `action` attribute | +| `ReturnUrl` | `string?` | No | `null` | Rendered as `` | +| `Error` | `string?` | No | `null` | Displayed as an error notice above the submit button | +| `ChildContent` | `RenderFragment?` | No | `null` | For `` | + +**Required:** inject `` via `ChildContent` and **validate `ReturnUrl` +server-side** before redirecting (open-redirect risk). + +```razor + + + +``` + +--- + +### `TechButton` + +Themed button wrapping Bootstrap `.btn` classes. + +| Parameter | Type | Required | Default | Notes | +|---|---|---|---|---| +| `Variant` | `ButtonVariant` | No | `Primary` | `Primary`, `Secondary`, `Danger`, `Ghost` | +| `Type` | `string` | No | `"button"` | HTML `type` attribute | +| `Busy` | `bool` | No | `false` | Disables button + shows spinner | +| `ChildContent` | `RenderFragment?` | No | `null` | Button label | +| (splatted) | `IDictionary?` | No | — | Passes through arbitrary HTML attributes | + +```csharp +public enum ButtonVariant { Primary, Secondary, Danger, Ghost } +``` + +--- + +### `TechCard` + +Panel with optional header, body, and footer slots. + +| Parameter | Type | Required | Notes | +|---|---|---|---| +| `Title` | `string?` | No | String title for the panel header (alternative to `Header` slot) | +| `Header` | `RenderFragment?` | No | Custom panel header (takes precedence over `Title`) | +| `ChildContent` | `RenderFragment?` | No | Panel body content | +| `Footer` | `RenderFragment?` | No | Panel footer (padded, top-bordered) | +| `Class` | `string?` | No | Additional CSS classes on the root `
` | + +--- + +### `TechField` + +Labeled form-field wrapper: label, input slot, hint text, and inline error. + +| Parameter | Type | Required | Notes | +|---|---|---|---| +| `Label` | `string` | Yes | `