Skip to content

fix(store): portal install drawer above global header#406

Open
sneumannb5 wants to merge 1 commit into
byte5ai:mainfrom
sneumannb5:feat/navbar-overlay
Open

fix(store): portal install drawer above global header#406
sneumannb5 wants to merge 1 commit into
byte5ai:mainfrom
sneumannb5:feat/navbar-overlay

Conversation

@sneumannb5

@sneumannb5 sneumannb5 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Portal the plugin install/config drawer to document.body so it renders above the global navigation header. Closes #399

Why

The drawer mounts inside the store sidebar's position: sticky element, which always creates a stacking context —
trapping the drawer's z-50 below the header's z-40. At scroll-top the header covered the drawer's heading and Close
button. Portaling escapes that context; matches existing createPortal usage (CreateIssueButton, StreamToasts).

Test plan

  • npm run typecheck — passed (drawer file clean)
  • npm run lint — pending
  • npm run test — pending
  • manual: open plugin detail → start install → scroll to top → heading + Close visible and clickable above header

Risk / blast radius

Low. Single client component; no schema/API/env/CI changes. Drawer only mounts on interaction (phase.kind !== 'idle')
→ SSR-safe.

@ConnysCode ConnysCode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Approve

Nothing to change before merge. The diagnosis is right and the fix is minimal; I verified the parts that usually break a portal-to-body change and they hold.

Verified

  • Theming survives the portal. --paper, --ink, --rule* etc. are declared on :root (web-ui/app/_lib/theme.css:37,147,151), so they cascade to document.body and the portaled subtree — the drawer keeps its styling and theme/palette overrides (:root[data-theme], :root[data-palette]).
  • Stacking fix is sound. The header is relative z-40 under body-level context providers (web-ui/app/layout.tsx:96); the portaled drawer is a fixed z-50 child of <body>, so both resolve at the root stacking context and z-50 > z-40 wins. (The header's backdrop-blur is itself a backdrop-filter → own stacking context, which is exactly why escaping via portal — not a z-index bump — is the correct fix.)
  • SSR-safe. InstallDrawer is gated by {drawerOpen ? … : null} and drawerOpen is false for the initial phase.kind === 'idle' (InstallButton.tsx:90-94,173), so createPortal(…, document.body) never runs server-side — same shape as the sibling CreateIssueButton ({open && createPortal(dialog, document.body)}).
  • z-10 removal is safe. Inside the portal <div>, the backdrop <button> (absolute, auto z) precedes the <aside> (relative, auto z) in DOM order, so the aside still paints on top — the explicit z-10 was redundant.

Good, accurate code comment too — it captures the why (sticky always creates a stacking context) rather than just the what.

PR description — factcheck

Claim Reality
Test plan lists npm run lint and npm run test as [X] checked, but the same lines read "— pending" Contradictory — the boxes are ticked while the text says pending. Non-blocking, but please either run them and drop "pending", or uncheck the boxes so the record is honest. I couldn't run them here (no node_modules in the clone) so I did not independently confirm lint/test.
"Portals ... matches existing createPortal usage (CreateIssueButton, StreamToasts)" Confirmed — StreamToasts.tsx:278 and CreateIssueButton.tsx:597 both portal to document.body.
Nits (non-blocking)
  • InstallButton.tsx:596-598 — now that the drawer is a proper body-level role="dialog" aria-modal="true", it still has no focus trap, ESC-to-close, or focus-restore-on-close. This is pre-existing (the fix neither adds nor removes it) and out of scope here, so purely optional — worth a follow-up ticket. The sibling CreateIssueButton wires ESC via a useEffect keydown listener if you want a reference.
  • StreamToasts.tsx additionally guards with if (typeof document === 'undefined') return null before portaling. You don't need it here (the drawerOpen conditional already prevents any server render), just noting the two patterns differ.

(Reviewed against head a0aadc7.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Navigation bar overlays sidebar content

2 participants