From d8505d8e1f2a05878d4c4664c9d28a13e566df8d Mon Sep 17 00:00:00 2001 From: ejay-dev Date: Sun, 24 May 2026 01:14:52 +0930 Subject: [PATCH] fix(audit-sprint-6b): z-index scale broader adoption + --z-toast token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Background: app/globals.css already had a 10-value z-index scale, but adoption was sparse — only the product-tour overlays used the CSS vars, while modal/overlay/command-palette surfaces all hardcoded `z-50`, `z-[60]`, etc. Result was the chaotic survey: 280 z-index uses, peaks at z-[10000] for panic-level layers. This PR doesn't migrate all 30 ad-hoc modal surfaces (most are slated for replacement by the Sprint 4c primitives — fixing their z-index now is wasted work). It establishes the *primitives* and the *surfaces that will remain* on the token scale, so once Phase 2/3 modal migrations land, every dialog automatically sits in the right layer. Changes - app/globals.css: new `--z-toast: 110` token between `--z-tour` (100) and `--z-debug` (120). Toasts must sit above tour overlays so a critical toast firing during a product tour stays visible. Scale comment expanded with the why for each band. - components/ui/dialog.tsx, alert-dialog.tsx: overlay → var(--z-modal-backdrop), content → var(--z-modal). Was z-50 for both (broken — content would never render above its own backdrop on equal z; relied on DOM order). - components/ui/sheet.tsx: same treatment. - components/ui/toaster.tsx: sonner root → var(--z-toast). - components/CookieConsent.tsx: was z-[60] hardcoded → var(--z-overlay). Banner now sits above page chrome, below modals + tour + toast — correct precedence. - components/command-palette/CommandPalette.tsx: was z-50 for both backdrop and content → token-scaled. Cmd-K now plays nicely with a tour overlay calling it out. What this PR does NOT touch - The 23 remaining ad-hoc modal surfaces (per the modal-audit agent's inventory). Each is slated for replacement by Dialog/AlertDialog/ Sheet in Phase 2/3 — fixing their z-index now would be reverted when the migration happens. The new primitives already use the token scale, so the migration is a net win on z-index too. - The 183 z-10 / 38 z-50 page-decoration uses. Most are sticky headers and visual layering, not modal-scoped, no conflict to fix. - tailwind.config.ts: not extended with z- utility classes. Inline style stays cleaner here because CSS variables in Tailwind's `extend.zIndex` require either arbitrary-value syntax or a rebuild step; inline style sidesteps both. Built on top of fix/audit-sprint-4c-modal-primitives (PR #167) because the Dialog/AlertDialog/Toaster primitives live there. Validation - tsc -p tsconfig.typecheck.json: clean - eslint: 0 errors, 18 warnings (baseline) - jest: 5319/5334 pass (no test changes) Test plan - Open a Dialog while a product tour is running — tour overlay should be above the dialog backdrop but below an active toast. - Trigger toast.error() while a Sheet is open — toast visible at bottom-right above the sheet. - Open command palette while CookieConsent is showing — command palette eats the banner correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/globals.css | 16 +++++++++++++++- components/CookieConsent.tsx | 5 ++++- components/command-palette/CommandPalette.tsx | 9 +++++++-- components/ui/alert-dialog.tsx | 7 +++++-- components/ui/dialog.tsx | 9 +++++++-- components/ui/sheet.tsx | 7 +++++-- components/ui/toaster.tsx | 6 ++++++ 7 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app/globals.css b/app/globals.css index 6851d4005..e49c1e340 100644 --- a/app/globals.css +++ b/app/globals.css @@ -28,7 +28,20 @@ --space-2xl: 3rem; /* 48px */ --space-3xl: 4rem; /* 64px */ - /* Z-index scale — single source of truth */ + /* Z-index scale — single source of truth. + * Audit Sprint 6b (2026-05-23): the scale existed but adoption was + * spotty (most modal surfaces still hardcoded z-50, command palette + * z-50, cookie consent z-[60]). New rule: every fixed/absolute + * stacked element references a token here. Order is bottom → top: + * - dropdown/sticky/nav: page chrome + * - modal-backdrop/modal: app dialogs + * - popover/tooltip: rare anchored layers + * - overlay: cookie consent, generic full-bleed callouts + * - tour: product-tour overlays (must sit above modals so a tour + * can call out a modal) + * - toast: sonner notifications must be ABOVE tour so a critical + * toast that fires during a tour is still visible + * - debug: dev-only indicators */ --z-base: 1; --z-dropdown: 10; --z-sticky: 20; @@ -39,6 +52,7 @@ --z-tooltip: 80; --z-overlay: 90; --z-tour: 100; + --z-toast: 110; --z-debug: 120; /* Motion tokens — mirrors config/motion.ts */ diff --git a/components/CookieConsent.tsx b/components/CookieConsent.tsx index decb9f1eb..0bccd1778 100644 --- a/components/CookieConsent.tsx +++ b/components/CookieConsent.tsx @@ -68,7 +68,10 @@ export default function CookieConsent() { // Not aria-modal — we don't want to trap focus; we want the // banner to be the FIRST natural stop after Skip-to-main so a // keyboard user can dismiss it without tabbing through the page. - className="fixed inset-x-0 bottom-0 z-[60] px-4 pb-4 sm:px-6 sm:pb-6" + // Audit Sprint 6b: was z-[60] hardcoded. Use --z-overlay (90) so + // the banner sits above page chrome but below modals + tour + toast. + style={{ zIndex: 'var(--z-overlay)' }} + className="fixed inset-x-0 bottom-0 px-4 pb-4 sm:px-6 sm:pb-6" >
diff --git a/components/command-palette/CommandPalette.tsx b/components/command-palette/CommandPalette.tsx index a567a51ec..041a8f86d 100644 --- a/components/command-palette/CommandPalette.tsx +++ b/components/command-palette/CommandPalette.tsx @@ -497,7 +497,11 @@ export function CommandPalette() { animate="visible" exit="exit" transition={{ duration: 0.15 }} - className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm" + // Audit Sprint 6b: token-scaled. Command palette is a + // top-level modal; uses the same backdrop/content tokens + // as the shared dialog primitive. + style={{ zIndex: 'var(--z-modal-backdrop)' }} + className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setOpen(false)} /> @@ -513,7 +517,8 @@ export function CommandPalette() { role="dialog" aria-modal="true" aria-label="Command palette" - className="fixed inset-0 z-50 flex items-start justify-center px-4 pt-[15vh] sm:pt-[20vh]" + style={{ zIndex: 'var(--z-modal)' }} + className="fixed inset-0 flex items-start justify-center px-4 pt-[15vh] sm:pt-[20vh]" >