From 44a683f3a5e17bb1f5308b4e9acc9cfb6cfdb9cd Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:33:13 -0700 Subject: [PATCH] fix(ui): unify overlays to shared z-50 tier, stack by portal order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All default-portalled overlays (Dialog, Popover, DropdownMenu, ContextMenu, Select) now share z-50, with stacking determined by portal mount order. This fixes resource popovers covering confirm dialogs opened from them. Exceptions: - Tooltip at z-90 (pointer-events-none, must stay readable) - Tours at z-70 (pointer-events-none spotlight) - Onboarding at z-100 (full-screen with private z-110–z-150 ladder) Preemptive modals keep z-140/z-150 to clear onboarding. Updated STYLEGUIDE.md with new policy. --- docs/STYLEGUIDE.md | 14 ++++++++++++++ .../src/components/agent/AgentCombobox.tsx | 4 +--- .../components/link-routing-preference-dialog.tsx | 2 ++ .../components/settings/SshPassphraseDialog.tsx | 4 ++-- .../components/settings/jira-integration-card.tsx | 2 -- .../settings/task-tracker-integration-cards.tsx | 2 -- .../sidebar/WorkspaceStatusAppearancePopover.tsx | 1 + .../UsagePercentageDisplayChangeNotice.tsx | 6 +++--- .../status-bar/ports-status-popover-rows.tsx | 2 +- .../status-bar/resource-usage-popover-summary.tsx | 6 +++--- .../status-bar/resource-usage-session-rows.tsx | 6 +----- .../tab-bar/tab-bar-static-create-menu.tsx | 2 +- src/renderer/src/components/ui/context-menu.tsx | 5 +++-- src/renderer/src/components/ui/dialog.tsx | 1 + src/renderer/src/components/ui/dropdown-menu.tsx | 5 +++-- src/renderer/src/components/ui/popover.tsx | 3 ++- src/renderer/src/components/ui/select.tsx | 3 ++- 17 files changed, 40 insertions(+), 28 deletions(-) diff --git a/docs/STYLEGUIDE.md b/docs/STYLEGUIDE.md index e21aaa89228..e95c119479f 100644 --- a/docs/STYLEGUIDE.md +++ b/docs/STYLEGUIDE.md @@ -106,6 +106,20 @@ Orca uses shadows sparingly. Three levels in practice: Don't add a fourth level. If something needs more emphasis than "floating," you're probably reaching for the focus `ring` instead. +### Overlay z-order + +Every default-portalled overlay — modal (`Dialog`, `Sheet`, command palette) and non-modal (`Popover`, `DropdownMenu`, `ContextMenu`, `Select`, `HoverCard`) — sits on one `z-50` tier, and stacking follows portal mount order: whatever mounts last paints on top. A confirm opened from a popover covers it; a picker opened from a dialog covers that. Ranked tiers cannot express this, because the two directions need opposite orders — so don't reintroduce per-layer gaps (`z-[60]`/`z-[70]`) or per-instance escalations to clear a sibling overlay. Components using a custom portal container own that container's stacking context. + +Three deliberate exceptions to that shared tier: + +- `Tooltip` at `z-[90]` — `pointer-events-none`, so it never blocks anything and must stay readable over whatever it describes. Don't override it per instance outside the onboarding ladder. +- Contextual tours at `z-[70]` — a `pointer-events-none` spotlight that has to be visible over the very surface it points at. Its `z-[75]` rings and `z-[80]` panel are scoped inside their own stacking context, so they claim no global level. +- The onboarding flow at `z-[100]`, an inline full-screen overlay that portalled content cannot reach. Its surfaces climb a private `z-[110]`–`z-[150]` ladder, and the two preemptive prompts that must interrupt onboarding (`SshPassphraseDialog`, `link-routing-preference-dialog`) top it out at `!z-[140]`/`!z-[150]`. + +Separately, a few transient full-screen layers sit above everything by design — the recent-tab switcher, drag ghosts, and the sidebar-resize capture shim. They are momentary and mostly `pointer-events-none`, so they never participate in overlay stacking. + +Outside those cases, a value above `z-50` is a smell: it pins a surface above the shared tier, so anything opened _from_ it renders behind it. Any new one must name the surface it clears in a `Why` comment. + ## Components Use the shadcn primitives in `src/renderer/src/components/ui/` before writing anything custom. The shadcn-style wrappers in this folder follow a consistent pattern: diff --git a/src/renderer/src/components/agent/AgentCombobox.tsx b/src/renderer/src/components/agent/AgentCombobox.tsx index 1c2ef729db9..6377face4cb 100644 --- a/src/renderer/src/components/agent/AgentCombobox.tsx +++ b/src/renderer/src/components/agent/AgentCombobox.tsx @@ -103,7 +103,7 @@ function AgentDefaultContextMenu({ return ( {children} - + {isDefault @@ -139,8 +139,6 @@ function renderItem({ ) return ( - // Why: z-[70] sits above PopoverContent's z-[60] so the right-click menu - // renders in front of the still-open combobox popover instead of behind it. {row} diff --git a/src/renderer/src/components/link-routing-preference-dialog.tsx b/src/renderer/src/components/link-routing-preference-dialog.tsx index cdf07d62962..213790a99cb 100644 --- a/src/renderer/src/components/link-routing-preference-dialog.tsx +++ b/src/renderer/src/components/link-routing-preference-dialog.tsx @@ -126,6 +126,8 @@ export function LinkRoutingPreferenceDialogProvider({ open={activeRequest !== null} onOpenChange={(open) => !open && settleActiveRequest(false)} > + {/* Why: a terminal link can fire mid-onboarding, so this prompt must clear + the onboarding overlay (z-100) rather than the default dialog z-50. */} !isOpen && void handleCancel()}> {/* Why: a credential prompt is a preemptive modal that must sit above any - open popover/menu (popover z-60, menus z-70) — the default dialog z-50 - would let a still-open picker cover the focused input. */} + open surface — including the onboarding overlay (z-100) — so it keeps + its elevated tier instead of the default dialog z-50. */} setTestResultBySite({})} - overlayClassName="z-[110]" - contentClassName="z-[120]" /> ) diff --git a/src/renderer/src/components/settings/task-tracker-integration-cards.tsx b/src/renderer/src/components/settings/task-tracker-integration-cards.tsx index 1ac07ac94e0..40442ad6a28 100644 --- a/src/renderer/src/components/settings/task-tracker-integration-cards.tsx +++ b/src/renderer/src/components/settings/task-tracker-integration-cards.tsx @@ -221,8 +221,6 @@ export function LinearIntegrationCard(): React.JSX.Element { onOpenChange={setDialogOpen} connectLabel="Add Linear access" onConnected={() => setTestResultByWorkspace({})} - overlayClassName="z-[110]" - contentClassName="z-[120]" /> ) diff --git a/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx b/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx index fb0ececf03a..306c68c1436 100644 --- a/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx @@ -57,6 +57,7 @@ export default function WorkspaceStatusAppearancePopover({ align="end" side="left" sideOffset={8} + // Why: the board keeps this picker above its sheet-local surface. className="z-[80] w-72 p-2" data-workspace-status-appearance-popover="" onOpenAutoFocus={(event) => event.preventDefault()} diff --git a/src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.tsx b/src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.tsx index 437afc314be..68db8cdbc7f 100644 --- a/src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.tsx +++ b/src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.tsx @@ -147,9 +147,9 @@ export function UsagePercentageDisplayChangeNotice({ ? createPortal(
{disabled ? {button} : button} - + {tooltipLabel} diff --git a/src/renderer/src/components/status-bar/resource-usage-popover-summary.tsx b/src/renderer/src/components/status-bar/resource-usage-popover-summary.tsx index 6901ae19a0f..342b1f22f1d 100644 --- a/src/renderer/src/components/status-bar/resource-usage-popover-summary.tsx +++ b/src/renderer/src/components/status-bar/resource-usage-popover-summary.tsx @@ -157,7 +157,7 @@ export function renderResourceUsageSummary({ {formatCpu(totalCpu)} - + {translate( 'auto.components.status.bar.ResourceUsageStatusSegment.1fedf94eae', 'Combined CPU load. Values above 100% mean more than one core is working at once.' @@ -177,7 +177,7 @@ export function renderResourceUsageSummary({ - + {memoryMetricCopy.description} @@ -199,7 +199,7 @@ export function renderResourceUsageSummary({ - + {commitMetricCopy.description} diff --git a/src/renderer/src/components/status-bar/resource-usage-session-rows.tsx b/src/renderer/src/components/status-bar/resource-usage-session-rows.tsx index 3efa332c294..03227ab30eb 100644 --- a/src/renderer/src/components/status-bar/resource-usage-session-rows.tsx +++ b/src/renderer/src/components/status-bar/resource-usage-session-rows.tsx @@ -225,11 +225,7 @@ export function WorktreeRow({ - + {isMainWorktree ? translate( 'auto.components.status.bar.ResourceUsageStatusSegment.946724a70a', diff --git a/src/renderer/src/components/tab-bar/tab-bar-static-create-menu.tsx b/src/renderer/src/components/tab-bar/tab-bar-static-create-menu.tsx index 45c39c2da55..24ed3c03552 100644 --- a/src/renderer/src/components/tab-bar/tab-bar-static-create-menu.tsx +++ b/src/renderer/src/components/tab-bar/tab-bar-static-create-menu.tsx @@ -126,7 +126,7 @@ export function TabBarStaticCreateMenu({ {newSimulatorShortcut} - + {translate( 'auto.components.tab.bar.TabBar.aea43b5748', 'Open the existing emulator tab.' diff --git a/src/renderer/src/components/ui/context-menu.tsx b/src/renderer/src/components/ui/context-menu.tsx index 5c574db1797..fe326a1ede6 100644 --- a/src/renderer/src/components/ui/context-menu.tsx +++ b/src/renderer/src/components/ui/context-menu.tsx @@ -58,7 +58,7 @@ function ContextMenuSubContent({