docs(ui-theme): spec, design tokens, shared contract
This commit is contained in:
@@ -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/<project>/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.
|
||||
@@ -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 `<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.
|
||||
@@ -0,0 +1,123 @@
|
||||
# UI Theme — Design Tokens
|
||||
|
||||
Canonical reference for every CSS custom property declared in `theme.css`. This is the
|
||||
human-readable index of the Technical-Light design system. The authoritative source is
|
||||
`ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/theme.css` (verified 2026-06-01,
|
||||
379 lines). Analogous to [`../auth/spec/CANONICAL-ROLES.md`](../../auth/spec/CANONICAL-ROLES.md)
|
||||
for the auth component.
|
||||
|
||||
The token block lives in `:root` (lines 39–77). Components reference these tokens —
|
||||
**no hardcoded hex values** appear in component markup or CSS. The only per-app override
|
||||
is `--accent` on the `ThemeShell` root element via the `Accent` parameter.
|
||||
|
||||
---
|
||||
|
||||
## Surface tokens
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--paper` | `#f4f4f1` | Page background — warm off-white, never pure white |
|
||||
| `--card` | `#ffffff` | Raised surfaces: cards, panel headers, table heads |
|
||||
|
||||
---
|
||||
|
||||
## Ink (text) tokens
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--ink` | `#1b1d21` | Primary text |
|
||||
| `--ink-soft` | `#5a6066` | Secondary text, form labels |
|
||||
| `--ink-faint` | `#8b9097` | Tertiary text, captions, units, nav eyebrow labels |
|
||||
|
||||
---
|
||||
|
||||
## Structure tokens
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--rule` | `#e4e4df` | Hairline borders, row dividers |
|
||||
| `--rule-strong` | `#d2d2cb` | Emphasised hairlines: bar underlines, pill borders |
|
||||
|
||||
---
|
||||
|
||||
## Accent tokens
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--accent` | `#2f5fd0` | Links, sort arrows, primary actions, active nav indicator |
|
||||
| `--accent-deep` | `#1e3f99` | Hover / pressed accent; raw-value emphasis |
|
||||
|
||||
> `--accent` is the **only token overridden per-app**. Pass it as `ThemeShell`'s
|
||||
> `Accent` parameter: `<ThemeShell Accent="#2f855a">` → emits
|
||||
> `style="--accent: #2f855a"` on the shell root, scoping the override to that subtree.
|
||||
|
||||
---
|
||||
|
||||
## Status tokens — foreground
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--ok` | `#2f9e44` | Success / healthy / connected state text + icon color |
|
||||
| `--warn` | `#e8920c` | Warning / degraded state text + icon color |
|
||||
| `--bad` | `#e03131` | Error / faulted / disconnected state text + icon color |
|
||||
| `--idle` | `#868e96` | Unknown / offline / neutral state text + icon color |
|
||||
|
||||
---
|
||||
|
||||
## Status tokens — tinted backgrounds
|
||||
|
||||
Pair each with the matching foreground token above (e.g. `--ok-bg` background with `--ok`
|
||||
foreground text). Used by `.chip-ok`, `.chip-warn`, `.chip-bad`, `.chip-idle` classes.
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--ok-bg` | `#e9f6ec` | Success tinted background |
|
||||
| `--warn-bg` | `#fdf1dd` | Warning tinted background |
|
||||
| `--bad-bg` | `#fceaea` | Error tinted background |
|
||||
| `--idle-bg` | `#eef0f2` | Idle/neutral tinted background |
|
||||
|
||||
> The `Info` status variant (`StatusState.Info`, `chip-info`) is defined in `layout.css`
|
||||
> (not `theme.css`) and uses `--accent-deep` foreground on `#e7ecfb` background.
|
||||
|
||||
---
|
||||
|
||||
## Typography tokens
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--mono` | `'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace` | Monospaced stack — numeric / code values; tabular figures via `font-variant-numeric: tabular-nums` |
|
||||
| `--sans` | `'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif` | UI body text stack — all prose, labels, nav |
|
||||
|
||||
IBM Plex fonts are vendored (three `.woff2` in `wwwroot/fonts/`) — graceful system-font
|
||||
fallback operates if the fonts are unreachable.
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap 5 override tokens
|
||||
|
||||
These tokens override Bootstrap 5's `--bs-*` custom properties so Bootstrap components
|
||||
inherit the Technical-Light aesthetic. They are **harmless if Bootstrap is absent**.
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `--bs-body-bg` | `var(--paper)` | Bootstrap body background → paper |
|
||||
| `--bs-body-color` | `var(--ink)` | Bootstrap body text → primary ink |
|
||||
| `--bs-body-font-family` | `var(--sans)` | Bootstrap body font → IBM Plex Sans |
|
||||
| `--bs-body-font-size` | `0.9rem` | Bootstrap body size — slightly compact |
|
||||
| `--bs-primary` | `var(--accent)` | Bootstrap primary color → accent |
|
||||
| `--bs-border-color` | `var(--rule)` | Bootstrap border → hairline rule |
|
||||
| `--bs-emphasis-color` | `var(--ink)` | Bootstrap emphasis text → primary ink |
|
||||
|
||||
---
|
||||
|
||||
## Usage rules
|
||||
|
||||
1. **Never hand-pick hex values in feature CSS.** Use the tokens above or the utility
|
||||
classes (`.s-ok`, `.s-warn`, `.s-bad`, `.s-idle`, `.chip-*`, `.kv .v.*`).
|
||||
2. **One per-app override only.** Override `--accent` via `ThemeShell`'s `Accent`
|
||||
parameter. Do not override other tokens per-app.
|
||||
3. **Status is colour, not iconography.** The status palette (`--ok`, `--warn`, `--bad`,
|
||||
`--idle`) is the canonical way to communicate state. Use `StatusPill` for chips; use
|
||||
`.s-*` utility classes for inline text.
|
||||
4. **Token changes are breaking.** Renaming or removing a token requires a SemVer major
|
||||
bump of `ZB.MOM.WW.Theme` and a coordinated update in every consumer app.
|
||||
@@ -0,0 +1,214 @@
|
||||
# UI Theme — normalized target spec
|
||||
|
||||
Status: **Draft**. The single design the sister projects converge on. Derived from the
|
||||
three code-verified current-state docs (`../current-state/`). Goal is *path to shared
|
||||
code* (`../shared-contract/ZB.MOM.WW.Theme.md`), so each normalized section maps to a
|
||||
shared library seam.
|
||||
|
||||
## 0. Scope
|
||||
|
||||
**Normalized here:** the "Technical-Light" design token set; IBM Plex typography;
|
||||
the canonical side-rail layout shell; the component kit (shell, status pill, login card,
|
||||
common controls); delivery via the `ZB.MOM.WW.Theme` RCL.
|
||||
|
||||
**Explicitly NOT normalized** (domain-specific — keep per project): each app's `site.css`
|
||||
residual page layout, route/page content, and app-specific scoped `.razor.css` files.
|
||||
The kit owns the *chrome and tokens*, not the app's domain screens. Authorization logic
|
||||
and interactive nav-state persistence (e.g. OtOpcUa's cookie-persisted rail sections)
|
||||
are also per-project.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design tokens
|
||||
|
||||
All color, typography, and structural values are expressed as **CSS custom properties**
|
||||
declared on `:root` in `theme.css`. Components carry **no hardcoded hex values** —
|
||||
everything references these tokens. Bootstrap 5 `--bs-*` variables are overridden to
|
||||
align Bootstrap's defaults with the Technical-Light palette.
|
||||
|
||||
The **one per-app override** allowed is `--accent` on the `ThemeShell` root element
|
||||
(passed via the `Accent` parameter), giving each app a distinct primary color while
|
||||
sharing all other tokens.
|
||||
|
||||
See [`DESIGN-TOKENS.md`](DESIGN-TOKENS.md) for the full enumeration.
|
||||
|
||||
**Canonical token groups:**
|
||||
- **Surface** — `--paper`, `--card`
|
||||
- **Ink (text)** — `--ink`, `--ink-soft`, `--ink-faint`
|
||||
- **Structure** — `--rule`, `--rule-strong`
|
||||
- **Accent** — `--accent`, `--accent-deep`
|
||||
- **Status** — `--ok`, `--warn`, `--bad`, `--idle` (+ `-bg` variants)
|
||||
- **Typography** — `--sans` (IBM Plex Sans), `--mono` (IBM Plex Mono)
|
||||
- **Bootstrap overrides** — `--bs-body-bg`, `--bs-body-color`, `--bs-body-font-family`,
|
||||
`--bs-body-font-size`, `--bs-primary`, `--bs-border-color`, `--bs-emphasis-color`
|
||||
|
||||
---
|
||||
|
||||
## 2. Typography
|
||||
|
||||
IBM Plex is **vendored** (three `.woff2` files in the RCL's `wwwroot/fonts/`); no CDN
|
||||
dependency so air-gapped fleet deployments keep working.
|
||||
|
||||
| Font | Weight | File |
|
||||
|---|---|---|
|
||||
| IBM Plex Sans | 400 (regular) | `ibm-plex-sans-400.woff2` |
|
||||
| IBM Plex Sans | 600 (semibold) | `ibm-plex-sans-600.woff2` |
|
||||
| IBM Plex Mono | 500 (medium) | `ibm-plex-mono-500.woff2` |
|
||||
|
||||
The `@font-face` declarations in `theme.css` use **`url('../fonts/ibm-plex-*.woff2')`**
|
||||
— the correct relative path from `css/theme.css` to `fonts/`. This is the **canonical
|
||||
path**; per-app copies that use `url('fonts/…')` or `url('/fonts/…')` are incorrect
|
||||
(OtOpcUa's `url('fonts/…')` causes a latent 404 silently masked by system-font fallback).
|
||||
The RCL fixes this permanently for all consumers.
|
||||
|
||||
---
|
||||
|
||||
## 3. Canonical side-rail layout
|
||||
|
||||
The one canonical layout is a **side rail** (not a top nav bar). Layout structure:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ .app-shell (flex-row on lg+; flex-col on sm) │
|
||||
│ ┌──────────────────┐ ┌───────────────────────┐ │
|
||||
│ │ nav.side-rail │ │ main.page │ │
|
||||
│ │ .brand │ │ (page body / @Body) │ │
|
||||
│ │ [Nav slot] │ │ │ │
|
||||
│ │ .rail-foot │ │ │ │
|
||||
│ └──────────────────┘ └───────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Rail width / breakpoint:** the rail collapses to a hamburger toggle (`data-bs-toggle=collapse`)
|
||||
below Bootstrap's `lg` breakpoint. Above `lg`, it is always visible.
|
||||
|
||||
**`ThemeShell` is a component, not a layout.** `@layout` in Blazor cannot accept
|
||||
parameters. Each app therefore keeps a thin 3-line `MainLayout : LayoutComponentBase`
|
||||
that delegates to `<ThemeShell>` with its per-app `Product`, `Accent`, `Nav`, and
|
||||
`RailFooter` values (see §4 and [`../shared-contract/ZB.MOM.WW.Theme.md`](../shared-contract/ZB.MOM.WW.Theme.md)).
|
||||
|
||||
Nav sections within the rail are CSS-only collapsibles (`<details open>`). Apps that need
|
||||
interactive expand-state persistence (e.g. OtOpcUa's cookie-persisted nav) may keep a
|
||||
bespoke interactive `NavSection`; the RCL's `NavRailSection` works without JS and is
|
||||
compatible with static Blazor SSR.
|
||||
|
||||
---
|
||||
|
||||
## 4. Component contract
|
||||
|
||||
Namespace: `ZB.MOM.WW.Theme`. All components are themed via CSS custom properties —
|
||||
no inline colors. One `@using ZB.MOM.WW.Theme` covers every component and enum.
|
||||
|
||||
### Static-asset entry point
|
||||
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| `ThemeHead` | Emits `<link>` tags for `theme.css` and `layout.css`. Drop in `<head>` **after** Bootstrap. |
|
||||
|
||||
### Layout shell
|
||||
|
||||
| Component / Type | Key parameters | Notes |
|
||||
|---|---|---|
|
||||
| `ThemeShell` | `Product`*, `Accent`, `Logo`, `Nav`, `RailFooter`, `ChildContent` | Canonical side-rail chassis. Not a `LayoutComponentBase` — delegated to from `MainLayout`. `Accent` overrides `--accent` for the shell subtree. |
|
||||
| `BrandBar` | `Product`*, `Logo` | Brand glyph + product name; rendered in the rail header inside `ThemeShell`. |
|
||||
| `NavRailItem` | `Href`*, `Text`*, `Icon`, `Match` | Wraps `<NavLink class="rail-link">`. Active state via Blazor `NavLink`. |
|
||||
| `NavRailSection` | `Title`*, `Expanded` (default `true`), `ChildContent` | CSS-only `<details>` collapsible group; no JS, works in static SSR. |
|
||||
|
||||
**Thin-MainLayout delegation pattern** (required; see §3):
|
||||
|
||||
```razor
|
||||
@* Components/Layout/MainLayout.razor *@
|
||||
@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>@* session info / sign-out *@</RailFooter>
|
||||
<ChildContent>@Body</ChildContent>
|
||||
</ThemeShell>
|
||||
```
|
||||
|
||||
### Widgets
|
||||
|
||||
| Component / Type | Key parameters | Notes |
|
||||
|---|---|---|
|
||||
| `StatusPill` | `State`* (`StatusState`), `ChildContent` | Inline chip. `StatusState` enum: `Ok`, `Warn`, `Bad`, `Idle`, `Info`. Maps state → token class (`chip-ok`, …). |
|
||||
| `LoginCard` | `Product`*, `Action` (default `/auth/login`), `ReturnUrl`, `Error`, `ChildContent` | Static form-POST sign-in card. `ChildContent` for `<AntiforgeryToken/>`. Validate `ReturnUrl` server-side (open-redirect risk). |
|
||||
| `TechButton` | `Variant` (`ButtonVariant`), `Type`, `Busy`, `ChildContent`, splatted attrs | `ButtonVariant` enum: `Primary`, `Secondary`, `Danger`, `Ghost`. `Busy` disables + shows spinner. |
|
||||
| `TechCard` | `Title`, `Header`, `ChildContent`, `Footer`, `Class` | Panel with optional head/body/footer slots. |
|
||||
| `TechField` | `Label`*, `Hint`, `Error`, `ChildContent` | Labeled input wrapper with hint text and inline error. |
|
||||
|
||||
\* `EditorRequired` parameter.
|
||||
|
||||
**Deliberately NOT included** (YAGNI / stays per-project): data grids, tree views,
|
||||
multi-select dropdowns, modals, toasts, audit components, page-specific layouts.
|
||||
|
||||
---
|
||||
|
||||
## 5. Delivery
|
||||
|
||||
The RCL ships as the single NuGet package `ZB.MOM.WW.Theme` (`.NET 10`, `Version 0.1.0`).
|
||||
|
||||
**Static assets** are served at `_content/ZB.MOM.WW.Theme/…` by ASP.NET's static-web-asset
|
||||
pipeline:
|
||||
|
||||
| Asset path | Contents |
|
||||
|---|---|
|
||||
| `_content/ZB.MOM.WW.Theme/css/theme.css` | Design tokens, typography, utility helpers |
|
||||
| `_content/ZB.MOM.WW.Theme/css/layout.css` | Side-rail layout, collapsible nav, `StatusPill` variants, card/field helpers |
|
||||
| `_content/ZB.MOM.WW.Theme/fonts/ibm-plex-sans-400.woff2` | IBM Plex Sans Regular |
|
||||
| `_content/ZB.MOM.WW.Theme/fonts/ibm-plex-sans-600.woff2` | IBM Plex Sans SemiBold |
|
||||
| `_content/ZB.MOM.WW.Theme/fonts/ibm-plex-mono-500.woff2` | IBM Plex Mono Medium |
|
||||
|
||||
**Bootstrap 5 is not vendored** by the kit — each app keeps its own Bootstrap `<link>`.
|
||||
|
||||
**Adoption entry points:**
|
||||
1. Add `<PackageReference Include="ZB.MOM.WW.Theme">` in the app.
|
||||
2. In `App.razor` `<head>`, after Bootstrap: `<ThemeHead />`.
|
||||
3. Replace `MainLayout` with the thin-delegation pattern (§3/§4).
|
||||
4. Add `@using ZB.MOM.WW.Theme` to `_Imports.razor`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Shared vs per-project
|
||||
|
||||
**Shared (extracted into the RCL):**
|
||||
|
||||
| What | Where in RCL |
|
||||
|---|---|
|
||||
| Design tokens (`--paper`, `--ink`, `--accent`, `--ok`, …) | `wwwroot/css/theme.css` |
|
||||
| IBM Plex fonts (three `.woff2`) | `wwwroot/fonts/` |
|
||||
| Side-rail shell layout CSS | `wwwroot/css/layout.css` |
|
||||
| Side-rail shell components (`ThemeShell`, `BrandBar`, nav components) | `Components/` |
|
||||
| Status chip (`StatusPill`, `StatusState`) | `Components/` |
|
||||
| Login card (`LoginCard`) | `Components/` |
|
||||
| Common controls (`TechButton`, `TechCard`, `TechField`) | `Components/` |
|
||||
|
||||
**Per-project (NOT extracted):**
|
||||
|
||||
| What | Rationale |
|
||||
|---|---|
|
||||
| `site.css` page layout residual | App-specific page structure varies (body padding, two-column layouts, etc.) |
|
||||
| Page / route components | Domain content — not a UI kit concern |
|
||||
| Scoped `.razor.css` files | Component-specific overrides stay with the component they scope |
|
||||
| Authorization / session UI | Depends on per-project auth model (`ZB.MOM.WW.Auth`) |
|
||||
| Interactive nav-state persistence | Bespoke (OtOpcUa uses a cookie; ScadaBridge / MxGateway use JS state) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Acceptance
|
||||
|
||||
A project is considered **adopted** when all of the following hold:
|
||||
|
||||
1. `ZB.MOM.WW.Theme` NuGet package referenced; `ZB.MOM.WW.Theme` in `_Imports.razor`.
|
||||
2. `<ThemeHead />` in `App.razor` `<head>` (after Bootstrap); per-app `theme.css` copy and
|
||||
IBM Plex `.woff2` files deleted from `wwwroot/`.
|
||||
3. `MainLayout` replaced with the thin-delegation pattern wrapping `<ThemeShell>`.
|
||||
4. Nav rebuilt with `NavRailItem` / `NavRailSection`.
|
||||
5. Local `StatusBadge` / `StatusChip` component deleted; replaced by `<StatusPill>`.
|
||||
6. Login form replaced with `<LoginCard>` (static form POST preserved; `<AntiforgeryToken/>`
|
||||
inside `ChildContent`; `ReturnUrl` validated server-side).
|
||||
7. Per-app `site.css` page-layout residual kept; scoped `.razor.css` files kept unchanged.
|
||||
Reference in New Issue
Block a user