All 35 findings fixed in 544a6dd and marked Status: Resolved with resolution
notes. README regenerated: 0 pending / 35 total across 6 libraries.
15 KiB
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):
.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.