Context
Two header controls render a fixed-width Radix PopoverContent with no viewport clamp at all: NetworkSwitcher (apps/ui/src/components/metagraphed/network-switcher.tsx:84, className="w-80 p-3 space-y-3" — a fixed 320px panel) and SettingsPopover (apps/ui/src/components/metagraphed/settings-popover.tsx:40, className="w-72 p-3 space-y-4" — a fixed 288px panel). Neither has a max-w-[calc(100vw-...)] or w-[min(...)] fallback, and the shared underlying primitive (apps/ui/src/components/ui/popover.tsx:22, base class w-72) doesn't clamp either. Both trigger buttons render unconditionally in the always-visible header row (app-shell.tsx:144-163, a plain flex items-center gap-1 div, no responsive hiding). On a 375px viewport, align="end" anchors the panel near the trigger; Radix's collision-avoidance can reposition the panel but does not shrink its fixed width, so on narrow devices the panel overflows the viewport edge. This is the same bug class already tracked for NavOmnibox under #3454/#3457, except neither of these two components even has that issue's partial clamp fallback.
Requirement
Both NetworkSwitcher and SettingsPopover's popover panels must never render wider than the viewport, at any width down to 320px. Rather than patching each one's className individually (which would leave a third copy-pasted clamp to keep in sync if a future popover is added), extract one shared, viewport-clamped popover-content wrapper and have both components use it.
Deliverable
- New shared primitive: a thin wrapper around the existing
apps/ui/src/components/ui/popover.tsx PopoverContent — e.g. ClampedPopoverContent — that always applies a max-w-[calc(100vw-1.5rem)] (or equivalent w-[min(20rem,calc(100vw-1.5rem))]) class alongside whatever fixed width the caller passes, so the fixed width becomes a maximum, not the actual rendered width on narrow viewports.
- Files to change:
apps/ui/src/components/metagraphed/network-switcher.tsx:84 and apps/ui/src/components/metagraphed/settings-popover.tsx:40 — both PopoverContent usages swap to the new wrapper (or get the clamp class added directly, if a shared wrapper is judged like overkill for two call sites — reviewer's call, but a shared primitive is preferred since a third consumer is likely).
- What "done" looks like concretely: at 320-375px, opening either popover shows the full panel content within the viewport bounds, with no horizontal scroll and no content hidden behind the device edge.
Acceptance criteria
Non-goals
Size
Size: S — keep this PR small (aim for ≤10 files / ≤1000 LOC).
Part of #2542.
Context
Two header controls render a fixed-width Radix
PopoverContentwith no viewport clamp at all:NetworkSwitcher(apps/ui/src/components/metagraphed/network-switcher.tsx:84,className="w-80 p-3 space-y-3"— a fixed 320px panel) andSettingsPopover(apps/ui/src/components/metagraphed/settings-popover.tsx:40,className="w-72 p-3 space-y-4"— a fixed 288px panel). Neither has amax-w-[calc(100vw-...)]orw-[min(...)]fallback, and the shared underlying primitive (apps/ui/src/components/ui/popover.tsx:22, base classw-72) doesn't clamp either. Both trigger buttons render unconditionally in the always-visible header row (app-shell.tsx:144-163, a plainflex items-center gap-1div, no responsive hiding). On a 375px viewport,align="end"anchors the panel near the trigger; Radix's collision-avoidance can reposition the panel but does not shrink its fixed width, so on narrow devices the panel overflows the viewport edge. This is the same bug class already tracked forNavOmniboxunder #3454/#3457, except neither of these two components even has that issue's partial clamp fallback.Requirement
Both
NetworkSwitcherandSettingsPopover's popover panels must never render wider than the viewport, at any width down to 320px. Rather than patching each one's className individually (which would leave a third copy-pasted clamp to keep in sync if a future popover is added), extract one shared, viewport-clamped popover-content wrapper and have both components use it.Deliverable
apps/ui/src/components/ui/popover.tsxPopoverContent— e.g.ClampedPopoverContent— that always applies amax-w-[calc(100vw-1.5rem)](or equivalentw-[min(20rem,calc(100vw-1.5rem))]) class alongside whatever fixed width the caller passes, so the fixed width becomes a maximum, not the actual rendered width on narrow viewports.apps/ui/src/components/metagraphed/network-switcher.tsx:84andapps/ui/src/components/metagraphed/settings-popover.tsx:40— bothPopoverContentusages swap to the new wrapper (or get the clamp class added directly, if a shared wrapper is judged like overkill for two call sites — reviewer's call, but a shared primitive is preferred since a third consumer is likely).Acceptance criteria
network-switcher.tsxandsettings-popover.tsxboth appear in the diffNon-goals
NavOmnibox's own dropdown — that's Hide or collapse the NavOmnibox search input on narrow mobile viewports #3454/Add a mobile-safe minimum-width guard to the NavOmnibox 'Jump to' grid #3457's scope, already tracked and already has a partial clamp in place.hidden md:flexetc.) to either trigger button — the fix here is making the panel fit, not hiding the trigger.Size
Size: S — keep this PR small (aim for ≤10 files / ≤1000 LOC).
Part of #2542.