# Code Review — Theme

| Field | Value |
|-------|-------|
| Library | `ZB.MOM.WW.Theme/` |
| Packages | `ZB.MOM.WW.Theme` (Razor Class Library) |
| Component spec | `components/ui-theme/spec/SPEC.md` |
| Shared contract | `components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md` |
| Status | Reviewed |
| Last reviewed | 2026-06-01 |
| Reviewer | Claude (automated baseline) |
| Commit reviewed | `5f75cd4` |
| Open findings | 0 |

## Summary

`ZB.MOM.WW.Theme` is a small, clean, well-scoped Blazor Razor Class Library: ten flat-namespace
SSR components, two stylesheets (`theme.css` 379 lines, `layout.css` 191 lines), three vendored
IBM Plex `.woff2` fonts, and two public enums. The component C# is correct and minimal — stateless
render logic, no concurrency surface, no secret handling, sensible `[Parameter]`/`EditorRequired`
choices, and `TreatWarningsAsErrors`. The 32 bUnit tests (20 `[Fact]` + 12 `[Theory]` cases) exercise
the public render contract of every component plus a static-asset check on the corrected font path.
The standout security-sensitive area — `LoginCard` (open-redirect + antiforgery) — is handled
correctly by delegating both concerns to the consumer with explicit XML docs and in-markup security
notes, matching the spec. No findings are Critical or High. The findings are concentrated in two
themes: (1) one functional gap — the mobile hamburger relies on Bootstrap's collapse JS, an
undocumented runtime dependency that contradicts the "no JavaScript" framing; and (2) CSS / token
hygiene — a token-fidelity drift on `.chip-idle`, hardcoded hex values that violate the spec's
"no hardcoded hex" rule, an undefined `.rail-ico` class, orphan/dead nav CSS, and a near-total
absence of XML docs on the public parameter surface.

## Checklist coverage

| # | Category | Examined | Notes |
|---|----------|----------|-------|
| 1 | Correctness & logic bugs | ☑ | Component render logic correct; `open="@Expanded"` boolean-attribute binding behaves (tests confirm); chip/variant `switch` mappings complete. No issues found. |
| 2 | Public API surface & compatibility | ☑ | Flat `ZB.MOM.WW.Theme` namespace; minimal, intentional; `EditorRequired` applied; nullable annotations correct; no internal types leak. Surface matches contract. No issues found. |
| 3 | Concurrency & thread safety | ☑ | Stateless components, no shared mutable state, no `async`/statics/singletons. No issues found (UI kit). |
| 4 | Error handling & resilience | ☑ | No throwing paths; required params default to `string.Empty`; null `RenderFragment` slots are guarded (`is not null` / `IsNullOrEmpty`). No issues found. |
| 5 | Security & secret handling | ☑ | No secrets/PII. `LoginCard` open-redirect + CSRF correctly delegated to consumer with docs. `Accent` interpolates into a `style` attr but is developer-supplied (not untrusted). No issues found. |
| 6 | Performance & resource management | ☑ | No `IDisposable`, no allocations of note, fonts vendored once. No issues found. |
| 7 | Spec & shared-contract adherence | ☑ | Component/parameter API matches SPEC §4 and the shared contract. Drifts found: `.chip-idle` foreground (Theme-002) and "no hardcoded hex" rule (Theme-003). |
| 8 | Packaging, dependencies & project layout | ☑ | `net10.0` RCL, single package id `ZB.MOM.WW.Theme`, `FrameworkReference` only, version `0.1.0` (inherited from `Directory.Build.props`), static assets under `_content/`. No issues found. |
| 9 | Testing coverage | ☑ | 32 tests cover every component's render contract + corrected font path. CSS-class wiring (e.g. `.rail-ico`, orphan nav CSS) is not asserted, which is how Theme-004/005 slipped through. |
| 10 | Documentation & XML docs | ☑ | README/contract accurate. XML docs present only on `LoginCard`; absent on the other 9 components, all their parameters, and both enums (Theme-006). |

## Findings

### Theme-001 — Mobile hamburger toggle silently depends on Bootstrap collapse JS

| | |
|--|--|
| Severity | Medium |
| Category | Spec & shared-contract adherence |
| Status | Resolved |
| Location | `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/ThemeShell.razor:5` |

**Description**

`ThemeShell` renders the narrow-viewport navigation toggle as a Bootstrap collapse trigger:
`<button ... data-bs-toggle="collapse" data-bs-target="#theme-rail" aria-expanded="false">`, with the
rail wrapped in `<div class="collapse d-lg-block" id="theme-rail">`. The expand/collapse behaviour
and the `aria-expanded` state transition are driven entirely by **Bootstrap's collapse JavaScript
bundle** at runtime. The shared contract states "No global JavaScript … `NavRailSection` is CSS-only"
and the spec emphasizes static-SSR/no-JS operation, while §5 only requires each app to keep its own
Bootstrap **`<link>`** (CSS) — Bootstrap *JS* is never mentioned. A consumer that includes only
Bootstrap CSS (a reasonable reading of the docs) gets a hamburger button that does nothing below the
`lg` breakpoint: the rail stays `display:none` (`.collapse`) and is unreachable on mobile/narrow
windows. There is no compile-time signal, and the hardcoded `aria-expanded="false"` never updates
without the JS, so assistive tech is also misinformed. This affects every consumer's small-viewport
navigation.

**Recommendation**

Either (a) document the Bootstrap-JS runtime requirement explicitly in the README/spec adoption steps
("the side-rail mobile toggle requires Bootstrap's collapse JS bundle"), or (b) make the toggle
JS-independent to match the "no JavaScript" promise — e.g. wrap the rail in a `<details>`/checkbox
CSS-only disclosure (the same pattern `NavRailSection` already uses), so collapse works in pure static
SSR. If keeping the Bootstrap-collapse approach, document that `aria-expanded` requires the JS to stay
accurate.

**Resolution**

Resolved in `544a6dd`, 2026-06-01 — removed the Bootstrap-collapse JS dependency: the shell is now a native CSS-only `<details>`/`<summary>` disclosure (no `data-bs-*`), force-shown on lg+; bUnit test asserts no Bootstrap collapse hooks.

### Theme-002 — `.chip-idle` foreground diverges from the documented token pairing

| | |
|--|--|
| Severity | Low |
| Category | Spec & shared-contract adherence |
| Status | Resolved |
| Location | `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/theme.css:177` |

**Description**

`DESIGN-TOKENS.md` ("Status tokens — tinted backgrounds") states the chip classes pair each tinted
background with its matching foreground token — "`--ok-bg` background with `--ok` foreground … Used by
`.chip-ok`, `.chip-warn`, `.chip-bad`, `.chip-idle`". The `--idle` token (`#868e96`) is correctly used
for `.s-idle` and the `.conn-pill .dot`, but `.chip-idle` uses `color: var(--ink-soft)` (`#5a6066`)
instead of `color: var(--idle)`:

```css
.chip-idle { color: var(--ink-soft); background: var(--idle-bg); border-color: var(--rule-strong); }
```

So the one idle chip variant does not follow the pairing rule the design-tokens doc spells out. It is
not a contrast or readability regression (`--ink-soft` is darker than `--idle`), but code and the
normalized token reference disagree, which is exactly the spec-drift category §7 asks to flag. Note the
`.chip-warn` foreground is likewise a hardcoded `#b56a00` rather than `var(--warn)` — see Theme-003.

**Recommendation**

Either change `.chip-idle` to `color: var(--idle)` to honor the documented pairing, or, if the darker
`--ink-soft` ink is the deliberate choice, amend `DESIGN-TOKENS.md` to record that `.chip-idle` uses
`--ink-soft` foreground (as it already does for the `Info` variant footnote).

**Resolution**

Resolved in `544a6dd`, 2026-06-01 — changed `.chip-idle` foreground from `var(--ink-soft)` to `var(--idle)`, honoring the DESIGN-TOKENS pairing rule; static-asset test asserts the pairing.

### Theme-003 — Hardcoded hex values in CSS contradict the "no hardcoded hex" rule

| | |
|--|--|
| Severity | Low |
| Category | Spec & shared-contract adherence |
| Status | Resolved |
| Location | `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/theme.css:174` (and theme.css:96-98,145-150,175-176,251-254,281,290-291,335,348,368-369,377; layout.css:123,128,184) |

**Description**

`SPEC.md` §1 and `DESIGN-TOKENS.md` both assert the rule: "Components carry **no hardcoded hex values**
— everything references these tokens" / "no hardcoded hex values appear in component markup or CSS".
In practice both stylesheets contain numerous literal hex colors outside the `:root` token block —
chip/agg border tints (`#c6e6cd`, `#eec3c3`, `#efd6a6`, `#bfe3c6`, `#f0d9ab`, `#f0c0c0`), chip/notice
foregrounds (`#b56a00`, `#8a5a00`), zebra/hover fills (`#fbfbf9`, `#f3f6fd`, `#eef2fc`), and the
info/dir tints (`#e7ecfb`, `#cdd9f7`). These are derived shades (lighter borders, hover washes) with no
corresponding token, so they are not re-themable via the token block and any per-app accent override
will not reach them. The rule as written is therefore aspirational, and the `DESIGN-TOKENS.md` Info
footnote itself already references a raw `#e7ecfb`, so the doc partially contradicts its own rule.

**Recommendation**

Reconcile code and doc: either promote the recurring derived shades to named tokens (e.g.
`--ok-border`, `--hover-bg`, `--info-bg`/`--info-border`) and reference them, or soften the spec wording
to "no hardcoded hex in **component markup**; CSS derives border/hover shades from tokens where
practical" so the rule matches reality. This keeps the "token changes are breaking" SemVer guarantee
honest.

**Resolution**

Resolved in `544a6dd`, 2026-06-01 — promoted every derived border/ink/wash shade to named `:root` tokens (`--ok-border`, `--warn-ink`, `--info-bg`, `--hover-bg`, `--active-bg`, `--zebra-bg`, …) and referenced them; no hex literal remains outside `:root` in either stylesheet, asserted by a new test.

### Theme-004 — `NavRailItem` emits a `.rail-ico` span that no stylesheet defines

| | |
|--|--|
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Resolved |
| Location | `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailItem.razor:4` |

**Description**

When the optional `Icon` slot is supplied, `NavRailItem` wraps it: `<span class="rail-ico">@Icon</span>`.
Neither `theme.css` nor `layout.css` defines a `.rail-ico` rule (confirmed by grep). The icon therefore
renders as an unstyled inline span — no sizing, alignment, or gap relative to the label text — so an
icon + text rail link will not line up the way the other rail styling implies. It is not a crash, but
the component ships a class hook with no backing style, which is a latent presentation bug for any
consumer that uses the `Icon` parameter (and none of the 32 tests cover the icon path).

**Recommendation**

Add a `.rail-ico` rule to `layout.css` (e.g. `display:inline-flex; width:1rem; margin-right:0.4rem;
color:var(--ink-faint);`) alongside the existing `.rail-link` rules, and add a bUnit test asserting the
`.rail-ico` span is emitted when `Icon` is supplied.

**Resolution**

Resolved in `544a6dd`, 2026-06-01 — added a `.rail-ico` rule to `layout.css` (inline-flex, sized, gapped, `--ink-faint`); bUnit tests assert the span is emitted when `Icon` is supplied and omitted otherwise.

### Theme-005 — Orphan and unstyled nav CSS classes in `layout.css`

| | |
|--|--|
| Severity | Low |
| Category | Documentation & XML docs |
| Status | Resolved |
| Location | `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/layout.css:75` (`.rail-eyebrow`), `:103` (`.rail-eyebrow-chevron`); `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor:5` (`.rail-eyebrow-label`) |

**Description**

`NavRailSection` renders `<summary class="rail-eyebrow-toggle"><span class="rail-eyebrow-label">@Title</span></summary>`
and `layout.css` provides the actual collapse chevron via `.rail-section > summary::before` (lines
178-181). That leaves several mismatched class hooks: `.rail-eyebrow` (line 75, non-toggle) and
`.rail-eyebrow-chevron` (lines 103-108) are defined in CSS but emitted by **no** component (dead CSS),
while `.rail-eyebrow-label` is emitted by the component but has **no** CSS rule (it merely inherits from
`.rail-eyebrow-toggle`). The result is two parallel chevron mechanisms (the styled-but-unused
`.rail-eyebrow-chevron` and the actually-used `::before`) and stylesheet cruft that misleads future
maintainers about how the section header is themed.

**Recommendation**

Remove the dead `.rail-eyebrow` and `.rail-eyebrow-chevron` rules (or wire `.rail-eyebrow-chevron` into
`NavRailSection` and drop the `::before` approach — pick one), and either add a `.rail-eyebrow-label`
rule or drop the now-redundant wrapper span. Keeping one chevron mechanism removes the ambiguity.

**Resolution**

Resolved in `544a6dd`, 2026-06-01 — removed the dead `.rail-eyebrow` and `.rail-eyebrow-chevron` rules and dropped the redundant `.rail-eyebrow-label` wrapper span in `NavRailSection`, leaving one chevron mechanism (`summary::before`).

### Theme-006 — Public component/parameter surface lacks XML documentation

| | |
|--|--|
| Severity | Low |
| Category | Documentation & XML docs |
| Status | Resolved |
| Location | `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/ThemeShell.razor:23` (and `BrandBar.razor`, `NavRailItem.razor`, `NavRailSection.razor`, `StatusPill.razor`, `TechButton.razor`, `TechCard.razor`, `TechField.razor`, `ThemeHead.razor`, `ButtonVariant.cs`, `StatusState.cs`) |

**Description**

This is a packaged library (`IsPackable=true`, `0.1.0`) consumed by three separate apps, and
`REVIEW-PROCESS.md` §2.10 calls for "XML doc accuracy and presence on the **public** API (these are
libraries — public docs matter)." Only `LoginCard` carries XML docs (on `ReturnUrl` and `ChildContent`).
The other nine components, all of their `[Parameter]` properties, and both public enums
(`ButtonVariant`, `StatusState`) have no `///` documentation, so consumers get no IntelliSense/tooltip
help on the parameter contract (e.g. that `ThemeShell.Accent` overrides the `--accent` token, that
`NavRailItem.Match` defaults to `Prefix`, or what each `StatusState`/`ButtonVariant` member maps to).
The README and shared contract document all of this, but that prose is not surfaced at the call site.

**Recommendation**

Add brief `///` summaries to the public components, their parameters, and the two enum types — at
minimum the non-obvious ones (`ThemeShell.Accent`, `NavRailItem.Match`, `TechButton.Busy`,
`TechCard.Title` vs `Header` precedence, and each enum member). Consider enabling
`GenerateDocumentationFile` so missing-doc warnings surface under the existing `TreatWarningsAsErrors`.

**Resolution**

Resolved in `544a6dd`, 2026-06-01 — added `///` summaries to all nine remaining components, every public `[Parameter]`, and both enums (`ButtonVariant`, `StatusState`); enabled `GenerateDocumentationFile` (CS1591 left off the error set for Razor-generated members) so docs build under `TreatWarningsAsErrors` with 0 warnings.
