From c7ef4c192d321407d06bb33e826bc8209cc271e1 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 15:48:09 -0500 Subject: [PATCH] refactor(desktop): name the overlay z-index ladder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESIGN.md already said app-wide surfaces must not compete through ad-hoc z-index literals, and the code disagreed: three overlapping numbering schemes, and comments narrating the fight ("defaults to z-130, renders UNDER the onboarding overlay (z-1300) ... bump it above with z-[1310]"). Picking a number meant reading someone else's near miss. Name the rungs — modal, over-modal, switcher, and the boot chain — and point the call sites at them. Every rung keeps the exact value it had, so nothing moves; what changes is that the next overlay has a name to reach for instead of a number to guess. Local stacking within a component stays on plain z-10/z-20. --- apps/desktop/DESIGN.md | 7 +++++- .../sidebar/projects/base-branch-picker.tsx | 2 +- .../desktop/src/app/command-palette/index.tsx | 4 ++-- .../components/provider-picker.tsx | 6 ++--- apps/desktop/src/app/session-switcher.tsx | 4 ++-- .../src/components/boot-failure-overlay.tsx | 4 ++-- .../components/desktop-install-overlay.tsx | 6 ++--- .../desktop/src/components/error-boundary.tsx | 2 +- .../src/components/first-run-remote-form.tsx | 2 +- .../gateway-connecting-overlay.test.tsx | 2 +- .../components/gateway-connecting-overlay.tsx | 2 +- apps/desktop/src/components/model-picker.tsx | 10 ++++----- apps/desktop/src/components/notifications.tsx | 6 ++--- .../src/components/onboarding/flow.tsx | 13 +++++------ .../src/components/onboarding/index.tsx | 2 +- .../desktop/src/components/session-picker.tsx | 4 ++-- apps/desktop/src/components/ui/dialog.tsx | 6 ++--- apps/desktop/src/components/ui/select.tsx | 2 +- apps/desktop/src/components/ui/tooltip.tsx | 2 +- apps/desktop/src/styles.css | 22 +++++++++++++++++++ 20 files changed, 67 insertions(+), 41 deletions(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 53075c5d49b..a9d585441ff 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -192,7 +192,12 @@ Notes: semantics when unifying appearance. - Respect `AppShell` overlay ownership. Persistent terminal/content layers, route overlays, dialogs, and boot surfaces must not compete through ad-hoc - z-index literals. + z-index literals. Pick a rung of the ladder in `styles.css` instead — + `--z-modal-backdrop` / `--z-modal` / `--z-modal-popover`, `--z-over-modal` + (toasts, tooltips, command surfaces) and `--z-over-modal-content`, + `--z-switcher-backdrop` / `--z-switcher`, then the boot chain + `--z-connecting` → `--z-onboarding` → `--z-setup` → `--z-crash`. Plain + `z-10`/`z-20` are still right for stacking *within* one component. ## Iconography & brand diff --git a/apps/desktop/src/app/chat/sidebar/projects/base-branch-picker.tsx b/apps/desktop/src/app/chat/sidebar/projects/base-branch-picker.tsx index c3870be0135..f07a3c04310 100644 --- a/apps/desktop/src/app/chat/sidebar/projects/base-branch-picker.tsx +++ b/apps/desktop/src/app/chat/sidebar/projects/base-branch-picker.tsx @@ -119,7 +119,7 @@ export function BaseBranchPicker({ {parts.after} - + (searchValue.toLowerCase().includes(search.toLowerCase()) ? 1 : 0)}> diff --git a/apps/desktop/src/app/command-palette/index.tsx b/apps/desktop/src/app/command-palette/index.tsx index e86bd2415e3..55e19fc9d66 100644 --- a/apps/desktop/src/app/command-palette/index.tsx +++ b/apps/desktop/src/app/command-palette/index.tsx @@ -875,13 +875,13 @@ export function CommandPalette() { {/* Transparent overlay: keeps click-away + focus trap, but no dim/blur. */} - + {t.commandCenter.paletteTitle} diff --git a/apps/desktop/src/app/pet-generate/components/provider-picker.tsx b/apps/desktop/src/app/pet-generate/components/provider-picker.tsx index 3279d7758aa..dad76c78d57 100644 --- a/apps/desktop/src/app/pet-generate/components/provider-picker.tsx +++ b/apps/desktop/src/app/pet-generate/components/provider-picker.tsx @@ -31,9 +31,9 @@ export function ProviderPicker() { - {/* The picker lives inside the pet-gen Dialog (z-130) and portals to body, - so lift its menu above the dialog or it opens behind it. */} - + {/* The picker lives inside the pet-gen Dialog and portals to body, so its + menu needs the rung above the modal or it opens behind the dialog. */} + {providers.map(provider => ( {/* Transparent click-catcher: click-away closes, but no dim/blur. */}
{ e.preventDefault() closeSwitcher() @@ -56,7 +56,7 @@ export function SessionSwitcher() { className={cn( HUD_POSITION, HUD_SURFACE, - 'dt-portal-scrollbar z-[220] max-h-[min(22rem,64vh)] w-[min(19rem,calc(100vw-2rem))] select-none overflow-y-auto p-1' + 'dt-portal-scrollbar z-(--z-switcher) max-h-[min(22rem,64vh)] w-[min(19rem,calc(100vw-2rem))] select-none overflow-y-auto p-1' )} > {sessions.map((session, i) => { diff --git a/apps/desktop/src/components/boot-failure-overlay.tsx b/apps/desktop/src/components/boot-failure-overlay.tsx index ea5ca27a106..f07366f2898 100644 --- a/apps/desktop/src/components/boot-failure-overlay.tsx +++ b/apps/desktop/src/components/boot-failure-overlay.tsx @@ -284,7 +284,7 @@ export function BootFailureOverlay() { if (view === 'connect') { return ( -
+
{/* Subtle back affordance (projects/overlay idiom): muted → foreground on hover, no divider. */} @@ -307,7 +307,7 @@ export function BootFailureOverlay() { } return ( -
+
diff --git a/apps/desktop/src/components/desktop-install-overlay.tsx b/apps/desktop/src/components/desktop-install-overlay.tsx index 4941f6dd6bc..4f814832395 100644 --- a/apps/desktop/src/components/desktop-install-overlay.tsx +++ b/apps/desktop/src/components/desktop-install-overlay.tsx @@ -398,7 +398,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP if (state.setupChoice) { return ( -
+
@@ -478,7 +478,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP const platformLabel = ups.platform === 'darwin' ? 'macOS' : ups.platform === 'linux' ? 'Linux' : ups.platform return ( -
+

{copy.oneTimeTitle}

{copy.unsupportedDesc(platformLabel)}

@@ -547,7 +547,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP const currentElapsed = typeof currentStartedAt === 'number' ? formatElapsed(now - currentStartedAt) : '' return ( -
+
{/* Header -- always visible, never scrolls */}
diff --git a/apps/desktop/src/components/error-boundary.tsx b/apps/desktop/src/components/error-boundary.tsx index 87b6b7743c5..f89f1d50673 100644 --- a/apps/desktop/src/components/error-boundary.tsx +++ b/apps/desktop/src/components/error-boundary.tsx @@ -56,7 +56,7 @@ function RootErrorFallback({ error, reset }: ErrorBoundaryFallbackProps) { const { t } = useI18n() return ( -
+
+
diff --git a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx index 508dfb27f33..a63fd1c72b3 100644 --- a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +++ b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx @@ -10,7 +10,7 @@ import { BootFailureOverlay } from './boot-failure-overlay' import { GatewayConnectingOverlay } from './gateway-connecting-overlay' // Repro for the "remote gateway → stuck on CONNECTING, no way to settings" -// report. The connecting overlay (z-1200, full-screen, pointer-events on) used +// report. The connecting overlay (full-screen, pointer-events on) used // to be shown whenever `gatewayState !== 'open' && !boot.error`. The ONLY escape // hatch — BootFailureOverlay, which has "Use local gateway" / "Sign in" / // "Retry" — only renders when `boot.error` is set. diff --git a/apps/desktop/src/components/gateway-connecting-overlay.tsx b/apps/desktop/src/components/gateway-connecting-overlay.tsx index 0268755d563..34303bb52cf 100644 --- a/apps/desktop/src/components/gateway-connecting-overlay.tsx +++ b/apps/desktop/src/components/gateway-connecting-overlay.tsx @@ -141,7 +141,7 @@ export function GatewayConnectingOverlay() { return (
diff --git a/apps/desktop/src/components/model-picker.tsx b/apps/desktop/src/components/model-picker.tsx index 65cc79bedf8..b38630d7e9b 100644 --- a/apps/desktop/src/components/model-picker.tsx +++ b/apps/desktop/src/components/model-picker.tsx @@ -28,10 +28,10 @@ interface ModelPickerDialogProps { onSelect: (selection: { provider: string; model: string }) => void profile?: string /** - * Optional class to apply to DialogContent. Use to override z-index when - * stacking the picker on top of another fixed overlay (e.g. the desktop - * onboarding overlay, which sits at z-1300; the default Dialog z-130 ends - * up rendering underneath and blocks pointer events). + * Optional class for DialogContent. Use it to lift the picker onto a higher + * rung of the overlay ladder when it opens over another fixed overlay (the + * desktop onboarding overlay, say) — on the default modal rung it renders + * underneath and blocks pointer events. */ contentClassName?: string } @@ -85,7 +85,7 @@ export function ModelPickerDialog({ // Open the full onboarding provider selector to add/switch a provider. // Reuses the entire onboarding flow (OAuth rows, API-key form, device-code, // model-confirm) instead of duplicating provider UI here. Closes the picker - // so the onboarding overlay (z-1300) isn't rendered underneath it. + // so the onboarding overlay isn't rendered underneath it. const addProvider = () => { startManualOnboarding() onOpenChange(false) diff --git a/apps/desktop/src/components/notifications.tsx b/apps/desktop/src/components/notifications.tsx index 4d4a3c1058f..64060323649 100644 --- a/apps/desktop/src/components/notifications.tsx +++ b/apps/desktop/src/components/notifications.tsx @@ -92,9 +92,9 @@ export function NotificationStack() { ) } -// Portaled to with a z above the Radix dialog layer (overlay z-[120], -// content z-[130]) — see the top-center variant below for why. -const REGION_BASE = 'pointer-events-none fixed z-[200] flex gap-2' +// Portaled to on the over-modal rung so a toast clears an open dialog — +// see the top-center variant below for why. +const REGION_BASE = 'pointer-events-none fixed z-(--z-over-modal) flex gap-2' // Primary stack: top-center, collapsed to the latest toast with a "+N more" // expander + clear-all — the noisy/important surface (errors, warnings, diff --git a/apps/desktop/src/components/onboarding/flow.tsx b/apps/desktop/src/components/onboarding/flow.tsx index a6499010c1a..c81419fc847 100644 --- a/apps/desktop/src/components/onboarding/flow.tsx +++ b/apps/desktop/src/components/onboarding/flow.tsx @@ -308,15 +308,14 @@ function ConfirmingModelPanel({
{/* - ModelPickerDialog defaults to z-130 on its content, which renders - UNDER the onboarding overlay (z-1300) and breaks pointer events. - Bump it above with z-[1310] so the picker sits on top of the - onboarding panel. The dialog's own dim-backdrop layer stays at - its default z-120 — the onboarding overlay is already dimming - the rest of the screen, so we don't want a second backdrop. + ModelPickerDialog's content sits on the modal rung, which is below the + onboarding overlay — it would render underneath and swallow pointer + events. Lift it to the rung above onboarding. Its own dim-backdrop + layer stays on the modal-backdrop rung: onboarding already dims the + rest of the screen, so a second backdrop would double up. */} - + {t.commandCenter.sections.sessions} diff --git a/apps/desktop/src/components/ui/dialog.tsx b/apps/desktop/src/components/ui/dialog.tsx index d84bb30c345..7c69bdad8c4 100644 --- a/apps/desktop/src/components/ui/dialog.tsx +++ b/apps/desktop/src/components/ui/dialog.tsx @@ -28,7 +28,7 @@ function DialogOverlay({ className, ...props }: React.ComponentProps