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