Files
scadaproj/components/ui-theme/spec/SPEC.md
T
2026-06-01 05:11:43 -04:00

10 KiB

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 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).

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):

@* 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.