# 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-11-09 (re-review; baseline 2026-06-01 at `5f75cd4`) | | Reviewer | Claude (2026-11-09 re-review, scadaproj#15; baseline: Claude automated) | | Commit reviewed | `91fb2b4` | | Open findings | 0 | ## Summary **2026-11-09 re-review (`91fb2b4`, scadaproj#15).** Re-reviewed at `91fb2b4` (2026-11-09) against the 2026-06-01 baseline `5f75cd4`; the kit is now `0.5.0`. New since the baseline: - the CSS-only `
` app shell, with the `::details-content` fix and the mobile shell fix; - `nav-state.js` (localStorage persistence of `NavRailSection`, active-link reveal, and a `MutationObserver` backstop for interactive render modes); - `ThemeScripts`; - the `TechButton` `Size`/`Class` extensions; - the stateless `TreeTable`/`ITreeRow`; - the upstreamed `.btn` sizing block and the 0.5.0 presentation classes. The C# components hold no state, implement no `IDisposable` and hold no JS interop references. There is no `MarkupString` anywhere, and every caller-supplied string is attribute- or text-encoded by Razor. `LoginCard` still delegates `ReturnUrl` validation and antiforgery to the host, with explicit docs. `TreeTable` documents its Bootstrap Icons dependency. The `MutationObserver` is never disconnected. That is fine: it is page-lifetime, created once per document, and the observe→mutate loop is guarded. The `
` shell relies on author `display` rules for browsers without `::details-content`. That could not be verified without those browsers, so it is not raised. All 60 bUnit/static-asset tests pass. There are two new findings: - **Theme-007 (Medium):** `TechButton` silently discards a caller-supplied `disabled` attribute. The explicit `disabled="@Busy"` is written after the splat, so a button the caller meant to disable renders enabled. Verified with a bUnit probe. - **Theme-008 (Low):** `nav-state.js` marks a section as wired with a DOM attribute. Blazor's enhanced-navigation DOM sync strips that attribute, so toggle listeners pile up on each navigation, and the extra listeners save the transient active-link reveal as the user's preference, which is the issue #2 regression. Verified by reading the code and simulating it in jsdom, with a control. **2026-06-01 baseline (`5f75cd4`).** `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 2026-11-09 re-review at `91fb2b4`: | # | Category | Examined | Notes | |---|----------|----------|-------| | 1 | Correctness & logic bugs | ☑ | `TechButton` drops caller `disabled` → Theme-007. Duplicate toggle listeners after enhanced navigation persist the transient reveal → Theme-008. `TreeTable` (keyed rows, leaf ARIA omission, invariant-culture depth style) and the `NavRailSection` slug are correct. | | 2 | Public API surface & compatibility | ☑ | Additive in 0.2.0–0.5.0: `ButtonSize`, `TechButton.Size`/`Class`, `TreeTable`/`ITreeRow`, `NavRailSection.Key`, `LoginCard.Heading`, `ThemeScripts`. No removals. The `CaptureUnmatchedValues` contract is not fully honoured for `disabled` (Theme-007). | | 3 | Concurrency & thread safety | ☑ | Stateless components; the JS is single-threaded with idempotent `apply()`. No issues found. | | 4 | Error handling & resilience | ☑ | `localStorage` access is wrapped in try/catch, and the script degrades when `window.Blazor` or `MutationObserver` is absent. No issues found. | | 5 | Security & secret handling | ☑ | No `MarkupString` and no raw HTML. `Accent` goes into a `style` attribute but is developer-supplied. `LoginCard` `ReturnUrl` is echoed into an encoded hidden field, and the open-redirect check stays documented as the host's job. localStorage holds only open/closed flags. No issues found. | | 6 | Performance & resource management | ☑ | The `MutationObserver` (whole-document `childList`/`subtree`) is page-lifetime and cheap per mutation. Toggle listeners accumulate one per enhanced navigation (Theme-008). No `IJSObjectReference` or `IDisposable` to leak. | | 7 | Spec & shared-contract adherence | ☑ | No-JS shell holds: native `
`, no Bootstrap JS. Tokens and chips are unchanged apart from documented additions. No new drift found. | | 8 | Packaging, dependencies & project layout | ☑ | 0.5.0 RCL, `FrameworkReference` only. `nav-state.js` is shipped and asserted by `StaticAssetsTests`. AngleSharp is pinned to 1.5.0 in the test project only. No issues found. | | 9 | Testing coverage | ☑ | 60 tests pass. Gaps: no test passes `disabled` through `TechButton`'s splat (Theme-007), and nothing exercises `nav-state.js` under DOM sync (Theme-008). | | 10 | Documentation & XML docs | ☑ | Theme-006 fix holds (XML docs on every component, parameter and enum). README documents `TreeTable`'s Bootstrap Icons and interactivity requirements. No issues found. |
2026-06-01 baseline checklist (5f75cd4) | # | 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: ``, with no `disabled`, so the button is clickable and fires `Delete`. A bUnit probe in the scratchpad confirmed this: `Render(p => p.AddUnmatched("disabled", true))` produced ``. No current family call site passes `disabled` (20 `.razor` files use `TechButton`), so this is latent. It is still a natural use of the documented splat, and any server-side authorization check would be the only backstop. **Recommendation** Compute the effective state as `Busy || callerDisabled`, where `callerDisabled` is read from `AdditionalAttributes["disabled"]` with the same case-insensitive lookup used for `class`. Better still, add an explicit `[Parameter] public bool Disabled`. Then render `disabled="@(Busy || Disabled)"`. Add a bUnit test for caller `disabled=true` with `Busy=false`. **Resolution** Resolved 2026-11-09 in the commit tagged `(scadaproj#44)` (Theme `0.5.1`, unpublished) — `TechButton` renders `disabled="@(Busy || CallerDisabled)"`, where `CallerDisabled` reads the splatted `disabled` case-insensitively with Blazor's own splat semantics (false/null omit it; `true`, `"disabled"`, `""` render it). Chose the lookup over a new `[Parameter] bool Disabled` because Blazor binds parameters case-insensitively: a bool parameter would turn an existing string `disabled="disabled"` splat into a runtime cast failure. Regression tests `TechButtonExtensionsTests.Caller_disabled_is_honoured_when_not_busy` (`true`, `"disabled"`, `""`), `Caller_disabled_false_leaves_the_button_enabled` and `Caller_disabled_toggles_off_on_rerender`; the first and third fail without the fix. ### Theme-008 — `nav-state.js` "wired" guard is a DOM attribute that Blazor enhanced-navigation DOM sync strips, so toggle listeners accumulate and the transient active-link reveal is persisted | | | |--|--| | Severity | Low | | Priority | P3 | | Category | Correctness & logic bugs | | Status | Resolved | | Location | `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/js/nav-state.js:7`, `:17-18`, `:39`, `:65-66` | **Description** `wire()` marks a section as initialised with the attribute `data-zbnav-initialized`. `apply()` uses `hasAttribute(INIT_ATTR)` as its only guard against adding a second `toggle` listener (commit `9e4dedc`, "guard nav-state.js against duplicate toggle listeners"). After an enhanced navigation, Blazor's DOM sync (`blazor.web.js` 10.0.1, its attribute synchronisation step) keeps the existing `
` element but removes every attribute that is not in the newly server-rendered HTML. That includes `data-zbnav-initialized`, which the server never renders. Then `enhancedload` calls `apply()`, which finds no marker and calls `wire()` again, adding another `toggle` listener to the same element. After N navigations the element carries N+1 listeners. The transient-reveal protection assumes a single listener. The first listener consumes `data-zbnav-transient` and returns. Every later listener sees no flag and writes `localStorage[zbnav:]`. **Failure scenario** 1. A user collapses the "Admin" rail section; `zbnav:admin` is saved as `"0"`. 2. They move around the app by enhanced (static-SSR) navigation. 3. They reach a page whose active link lives inside "Admin", for example from a link in the page body or a redirect. 4. `apply()` reveals the section and flags it transient. The duplicate listeners then overwrite the saved preference with `"1"`, so the section stays expanded on every later page. This is the issue #2 bug the transient flag exists to prevent. A jsdom simulation reproduced it, running the shipped `nav-state.js`, stripping the attribute the way DOM sync does, and firing `enhancedload` 3 times: - one collapse produced 4 `setItem` writes; - the reveal produced 3 writes of `"1"`, leaving `saved=1`; - the control run, with the attribute not stripped, produced 1 collapse write and 0 reveal writes, leaving `saved=0`. **Recommendation** Track wiring in a JS-side structure that DOM sync cannot touch, for example a module-level `WeakSet` of wired elements or an expando property (`el.__zbnavWired = true`), instead of a DOM attribute. Add a test that strips attributes, re-runs `apply()`, and asserts one write per toggle. **Resolution** Resolved 2026-11-09 in the commit tagged `(scadaproj#48)` (Theme `0.5.2`, unpublished) — `nav-state.js` now records wired sections in a JS `WeakSet` (expando fallback) instead of reading the `data-zbnav-initialized` attribute that Blazor's DOM sync strips; the attribute is still written but never used as a guard. Regression test `NavStateScriptTests` runs `nav-state.test.js` under Node with a minimal DOM stub: after 3 simulated enhanced navigations (attribute stripped) there must be 1 toggle listener, expand+collapse must be 2 writes, and the transient active-link reveal must write nothing and leave `zbnav:admin=0` (failed with 4 listeners without the fix). The test passes vacuously on machines without Node (noted in its summary).