Files
scadaproj/components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md
T
2026-06-01 05:11:43 -04:00

245 lines
8.5 KiB
Markdown

# 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 `<link>` tags for `theme.css` and `layout.css`. No parameters.
```razor
<ThemeHead />
```
Place in `App.razor` `<head>` **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
<ThemeShell Product="OtOpcUa" Accent="#2f5fd0">
<Nav>
<NavRailSection Title="Navigation">
<NavRailItem Href="/" Text="Overview" Match="NavLinkMatch.All" />
<NavRailItem Href="/clusters" Text="Clusters" />
</NavRailSection>
</Nav>
<RailFooter>
@* AuthorizeView session block / sign-out link *@
</RailFooter>
<ChildContent>@Body</ChildContent>
</ThemeShell>
```
---
### `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 `<NavLink class="rail-link">`.
| 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 `<details open>` — 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 `<input type="hidden" name="returnUrl">` |
| `Error` | `string?` | No | `null` | Displayed as an error notice above the submit button |
| `ChildContent` | `RenderFragment?` | No | `null` | For `<AntiforgeryToken/>` |
**Required:** inject `<AntiforgeryToken/>` via `ChildContent` and **validate `ReturnUrl`
server-side** before redirecting (open-redirect risk).
```razor
<LoginCard Product="OtOpcUa" Action="/auth/login" ReturnUrl="@safeUrl" Error="@errorMsg">
<AntiforgeryToken />
</LoginCard>
```
---
### `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<string,object>?` | 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 `<section class="panel">` |
---
### `TechField`
Labeled form-field wrapper: label, input slot, hint text, and inline error.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| `Label` | `string` | Yes | `<label>` text |
| `Hint` | `string?` | No | Rendered as `.form-text` below the input |
| `Error` | `string?` | No | Rendered as `.field-error.s-bad` below the input |
| `ChildContent` | `RenderFragment?` | No | The `<input>`, `<select>`, or other control |
---
## Notes
- **Bootstrap is not vendored.** Each app keeps its own Bootstrap `<link>`. The RCL's
`theme.css` overrides `--bs-*` tokens to align Bootstrap with Technical-Light but
does not ship Bootstrap itself.
- **No global JavaScript.** `NavRailSection` is CSS-only (`<details>`). Apps may add
their own `nav-state.js` for interactive expand-state if needed (OtOpcUa has one).
- **No auth logic.** The RCL is UI-only. Wire `LoginCard` to `ZB.MOM.WW.Auth` endpoints
in the app.
- **No data grids, modals, or domain-specific components.** These stay per-project.