245 lines
8.5 KiB
Markdown
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.
|