215 lines
10 KiB
Markdown
215 lines
10 KiB
Markdown
# 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.
|