diff --git a/packages/preview-site/src/shell/toolbar/CompactToolbar.tsx b/packages/preview-site/src/shell/toolbar/CompactToolbar.tsx index 8672d38..6461bfc 100644 --- a/packages/preview-site/src/shell/toolbar/CompactToolbar.tsx +++ b/packages/preview-site/src/shell/toolbar/CompactToolbar.tsx @@ -1,6 +1,7 @@ // Internal to the shell Toolbar — not part of the feature index barrel. -import { useState } from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import type { FrameSize } from '../store'; import { FRAME_SIZES, FRAME_SIZE_LABELS } from './frame-sizes'; @@ -42,6 +43,39 @@ export function CompactToolbar({ reloadPreview, }: CompactToolbarProps) { const [menuOpen, setMenuOpen] = useState(false); + const triggerRef = useRef(null); + // Fixed-position anchor for the portalled popover, in viewport + // coordinates. `right` is the distance from the viewport's right edge + // so the menu stays flush with the trigger's right side. + const [anchor, setAnchor] = useState<{ top: number; right: number }>({ + top: 0, + right: 0, + }); + + // The toolbar uses `backdrop-filter`, which makes it its own stacking + // context. An absolutely-positioned popover nested inside is therefore + // trapped *behind* the sibling
/preview iframe no matter how high + // its z-index. We portal the menu to to escape that context, and + // anchor it to the trigger's viewport rect with `position: fixed`. + useLayoutEffect(() => { + if (!menuOpen) return; + const place = () => { + const el = triggerRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + setAnchor({ + top: rect.bottom + 6, + right: Math.max(12, window.innerWidth - rect.right), + }); + }; + place(); + window.addEventListener('resize', place); + window.addEventListener('scroll', place, true); + return () => { + window.removeEventListener('resize', place); + window.removeEventListener('scroll', place, true); + }; + }, [menuOpen]); function jump(target: string) { navigateInPreview(target); @@ -102,6 +136,7 @@ export function CompactToolbar({ three control clusters (tab strip / overflow / reload) all read as one consistent 40px-tall row. */} - {menuOpen && ( + {menuOpen && createPortal( <> {/* Backdrop swallows the next pointer down so a tap on page background closes the menu without firing on - whatever was underneath. */} + whatever was underneath. Portalled to alongside the + menu so both escape the toolbar's backdrop-filter stacking + context (otherwise the menu paints behind the preview + iframe and the trigger looks dead). */} - + , + document.body )} );