Navbar identity dropdown renders translucent in dark mode (IdentityMenu) - needs fix + systemic popover guard #405

Closed
opened 2026-09-12 19:00:36 +00:00 by crueber · 3 comments
Owner

Navbar identity dropdown renders translucent in dark mode (IdentityMenu)

What's requested

The navbar account dropdown (IdentityMenu) opens a panel that is translucent in dark mode — page text scrolls visibly through it. Fix IdentityMenu AND add a systemic guard so no popover/dropdown can ever ship with a translucent panel background again.

Evidence (root cause)

  • web/src/components/IdentityMenu.jsx:119 — the menu panel is
    class="card absolute right-0 z-50 mt-1 grid max-h-96 w-52 …".
  • web/src/ui.css:26 — the shared .card utility resolves to
    bg-white dark:bg-zinc-900/70 — the dark-mode background is 70% alpha, translucent by design.
  • web/src/ui.css:165-171 — the repo already solved this for every other popover: a "Floating popovers" rule (from issues #37/#115, extended by #278) forces bg-white dark:bg-zinc-900 on an enumerated class list:
    .clone-body, .ref-drop, .tasks-drop, .notif-drop, .reaction-drop, .label-drop, .milestone-drop, .close-drop, .tag-drop.

IdentityMenu's dropdown carries none of those classes (the component predates none — it simply was never added to the sweep, consistent with the #390 restyle touching only the trigger). So the only background it gets is .card's zinc-900/70, and page content bleeds through in dark mode. Every other absolute .card panel in the tree (RefPicker, TasksOverlay, NotificationTray, ReactionMenu, LabelPicker, MilestonePicker, close menu, tag menu, clone box) carries one of the enumerated classes; IdentityMenu is the sole offender.

Architecture notes

  • The enumerated-class approach is the standing pattern, but it is exactly what let this one slip: the guard is a hand-maintained selector list, and any new dropdown that forgets to join the list re-derives the bug. The fix should make the safe behavior the default, not per-class opt-in.
  • Acceptable guard shapes (implementer's call, pick one and note it):
    1. Flip the default: make the floating-popover rule match structurally — e.g. apply the opaque background to any absolute/fixed-positioned .card (or a single .popover/.drop class applied in the shared panel recipe) and delete the enumeration.
    2. Keep the enumeration but add a unit test (web/test/unit/, same harness as web/test/unit/identity-nav.test.js) that reads web/src/components/*.jsx + web/src/pages/*.jsx and fails when a card class string contains absolute or fixed without one of the opaque classes present — the same static-assert style the repo already uses in unit tests.

Acceptance criteria

  • IdentityMenu dropdown renders opaque in dark mode (no bleed-through of page content), matching the other popover classes.
  • The chosen systemic guard is in place (structural rule or static test) and fails if a new absolute/fixed .card panel ships translucent.
  • No existing popover's appearance regresses (the enumerated classes stay effective or are subsumed by the new default).
  • Unit tests pass (make web test target used by CI).
# Navbar identity dropdown renders translucent in dark mode (IdentityMenu) ## What's requested The navbar account dropdown (`IdentityMenu`) opens a panel that is translucent in dark mode — page text scrolls visibly through it. Fix IdentityMenu AND add a systemic guard so no popover/dropdown can ever ship with a translucent panel background again. ## Evidence (root cause) - `web/src/components/IdentityMenu.jsx:119` — the menu panel is `class="card absolute right-0 z-50 mt-1 grid max-h-96 w-52 …"`. - `web/src/ui.css:26` — the shared `.card` utility resolves to `bg-white dark:bg-zinc-900/70` — the dark-mode background is **70% alpha, translucent by design**. - `web/src/ui.css:165-171` — the repo already solved this for every other popover: a "Floating popovers" rule (from issues #37/#115, extended by #278) forces `bg-white dark:bg-zinc-900` on an enumerated class list: `.clone-body, .ref-drop, .tasks-drop, .notif-drop, .reaction-drop, .label-drop, .milestone-drop, .close-drop, .tag-drop`. IdentityMenu's dropdown carries **none of those classes** (the component predates none — it simply was never added to the sweep, consistent with the #390 restyle touching only the trigger). So the only background it gets is `.card`'s `zinc-900/70`, and page content bleeds through in dark mode. Every other absolute `.card` panel in the tree (RefPicker, TasksOverlay, NotificationTray, ReactionMenu, LabelPicker, MilestonePicker, close menu, tag menu, clone box) carries one of the enumerated classes; IdentityMenu is the sole offender. ## Architecture notes - The enumerated-class approach is the standing pattern, but it is exactly what let this one slip: the guard is a hand-maintained selector list, and any new dropdown that forgets to join the list re-derives the bug. The fix should make the safe behavior the default, not per-class opt-in. - Acceptable guard shapes (implementer's call, pick one and note it): 1. Flip the default: make the floating-popover rule match structurally — e.g. apply the opaque background to any `absolute`/`fixed`-positioned `.card` (or a single `.popover`/`.drop` class applied in the shared panel recipe) and delete the enumeration. 2. Keep the enumeration but add a unit test (`web/test/unit/`, same harness as `web/test/unit/identity-nav.test.js`) that reads `web/src/components/*.jsx` + `web/src/pages/*.jsx` and fails when a `card` class string contains `absolute` or `fixed` without one of the opaque classes present — the same static-assert style the repo already uses in unit tests. ## Acceptance criteria - [ ] IdentityMenu dropdown renders opaque in dark mode (no bleed-through of page content), matching the other popover classes. - [ ] The chosen systemic guard is in place (structural rule or static test) and fails if a new absolute/fixed `.card` panel ships translucent. - [ ] No existing popover's appearance regresses (the enumerated classes stay effective or are subsumed by the new default). - [ ] Unit tests pass (`make` web test target used by CI).
crueber added this to the v1 milestone 2026-09-12 19:00:54 +00:00
Author
Owner

Fix PR: #408 (branch fix/issue-405). Chose guard option 1 (structural default .card.absolute/.card.fixed opaque) PLUS a static test pinning it (safest combination). IdentityMenu fixed with zero JSX churn; all 9 legacy popovers subsumed (still present, still floating .cards). Tests: targeted 39/39 green; full suite delta is exactly +4 new passing (13 pre-existing failures on main unchanged); vite build green with the opaque rule in compiled CSS. No new deps; doc decision appended (law 12). Not merging per instructions — ready for review.

Fix PR: https://git.packden.us/crueber/walhub/pulls/408 (branch fix/issue-405). Chose guard option 1 (structural default `.card.absolute/.card.fixed` opaque) PLUS a static test pinning it (safest combination). IdentityMenu fixed with zero JSX churn; all 9 legacy popovers subsumed (still present, still floating .cards). Tests: targeted 39/39 green; full suite delta is exactly +4 new passing (13 pre-existing failures on main unchanged); vite build green with the opaque rule in compiled CSS. No new deps; doc decision appended (law 12). Not merging per instructions — ready for review.
Author
Owner

REVIEW PR #408 (fix/issue-405) — verified in scratch worktrees, main worktree untouched. No browser (node tests + compiled-CSS reasoning, per instructions).

(1) IdentityMenu fixed, zero JSX churn — PASS. web/src/components/IdentityMenu.jsx:119 panel is card absolute right-0 z-50 ...; file untouched by the diff. card absolute matches the new .card.absolute selector, so it now renders opaque dark zinc-900 instead of .card's zinc-900/70.

(2) Structural rule correct — PASS. web/src/ui.css:178 .card.absolute, .card.fixed { @apply bg-white dark:bg-zinc-900; } is (0,2,0) vs .card's (0,1,0): wins regardless of order. Compiled CSS (vite build in scratch) confirms: .card.absolute,.card.fixed{background-color:var(--color-white)} and :is(.card.absolute,.card.fixed):where(.dark,.dark *){background-color:var(--color-zinc-900)} (fully opaque, no alpha) vs .card's dark color-mix(...70%...). Utilities-layer caveat is real (a bg-*/70 utility on the element would beat the components-layer rule) and guard test 3 (TRANSLUCENT_BG scan) covers exactly that.

(3) All 9 legacy popovers subsumed — PASS. Exhaustive grep finds 13 literal floating .card class strings (all absolute, zero fixed): clone-body, ref-drop x3, tasks-drop, tag-drop, reaction-drop, notif-drop, milestone-drop, label-drop x2, close-drop, + IdentityMenu. The deleted enumeration carried ONLY the opacity @apply (viewport max-width rule below still enumerates the hooks incl. .tray, untouched), so nothing else relied on the deleted block. Test 4 pins each hook still on a floating .card.

(4) Guard tests — PASS. popover-opaque.test.js: proven the structural-rule regex FAILS against origin/main's old enumeration CSS (guard catches regression); translucent-utility test, IdentityMenu pin, and legacy-presence test all pass (targeted popover-opaque + popover-viewport + identity-nav = 39/39). Milestone pin update in popover-viewport.test.js is faithful (same intent, asserts structural coverage + floating .card).

(5) No missed absolute/fixed .card — PASS. Only dynamic card class in the tree is pages/Notifications.jsx:137 (non-floating list row, no absolute/fixed) — correctly out of scope.

(6) Compiled CSS contains the rule — PASS (see (2)).

(7) No new deps; docs accurate — PASS. Diff touches only ui.css + 2 test files + docs/go/12_web_ui.md decision entry (law 12). Test imports are node stdlib only. Law 1/7/8 clean.

(8) Full suite — PASS with one stale-description note. Branch: 809 tests / 807 pass / 2 fail; pristine origin/main: 805 / 803 / 2 fail — identical failing files (smoke.test.js x2, need a live server; unrelated paths), delta is exactly the +4 new passing tests. The PR description's '13 failures' is stale (main has moved on since; HEAD b2dbf8d) — actual state is strictly better, no action needed.

No fixes required; nothing pushed. MERGE RECOMMENDATION: ready to merge.

REVIEW PR #408 (fix/issue-405) — verified in scratch worktrees, main worktree untouched. No browser (node tests + compiled-CSS reasoning, per instructions). (1) IdentityMenu fixed, zero JSX churn — PASS. web/src/components/IdentityMenu.jsx:119 panel is `card absolute right-0 z-50 ...`; file untouched by the diff. `card absolute` matches the new `.card.absolute` selector, so it now renders opaque dark zinc-900 instead of .card's zinc-900/70. (2) Structural rule correct — PASS. web/src/ui.css:178 `.card.absolute, .card.fixed { @apply bg-white dark:bg-zinc-900; }` is (0,2,0) vs .card's (0,1,0): wins regardless of order. Compiled CSS (vite build in scratch) confirms: `.card.absolute,.card.fixed{background-color:var(--color-white)}` and `:is(.card.absolute,.card.fixed):where(.dark,.dark *){background-color:var(--color-zinc-900)}` (fully opaque, no alpha) vs .card's dark `color-mix(...70%...)`. Utilities-layer caveat is real (a bg-*/70 utility on the element would beat the components-layer rule) and guard test 3 (TRANSLUCENT_BG scan) covers exactly that. (3) All 9 legacy popovers subsumed — PASS. Exhaustive grep finds 13 literal floating .card class strings (all absolute, zero fixed): clone-body, ref-drop x3, tasks-drop, tag-drop, reaction-drop, notif-drop, milestone-drop, label-drop x2, close-drop, + IdentityMenu. The deleted enumeration carried ONLY the opacity @apply (viewport max-width rule below still enumerates the hooks incl. .tray, untouched), so nothing else relied on the deleted block. Test 4 pins each hook still on a floating .card. (4) Guard tests — PASS. popover-opaque.test.js: proven the structural-rule regex FAILS against origin/main's old enumeration CSS (guard catches regression); translucent-utility test, IdentityMenu pin, and legacy-presence test all pass (targeted popover-opaque + popover-viewport + identity-nav = 39/39). Milestone pin update in popover-viewport.test.js is faithful (same intent, asserts structural coverage + floating .card). (5) No missed absolute/fixed .card — PASS. Only dynamic card class in the tree is pages/Notifications.jsx:137 (non-floating list row, no absolute/fixed) — correctly out of scope. (6) Compiled CSS contains the rule — PASS (see (2)). (7) No new deps; docs accurate — PASS. Diff touches only ui.css + 2 test files + docs/go/12_web_ui.md decision entry (law 12). Test imports are node stdlib only. Law 1/7/8 clean. (8) Full suite — PASS with one stale-description note. Branch: 809 tests / 807 pass / 2 fail; pristine origin/main: 805 / 803 / 2 fail — identical failing files (smoke.test.js x2, need a live server; unrelated paths), delta is exactly the +4 new passing tests. The PR description's '13 failures' is stale (main has moved on since; HEAD b2dbf8d) — actual state is strictly better, no action needed. No fixes required; nothing pushed. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #408 (review clean — structural rule + guard proven, all popovers subsumed, compiled CSS verified), merged. Closing.

Fixed by PR #408 (review clean — structural rule + guard proven, all popovers subsumed, compiled CSS verified), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#405
No description provided.