diff --git a/css/tailwind.source.css b/css/tailwind.source.css index fffa0708..07ab5cf6 100644 --- a/css/tailwind.source.css +++ b/css/tailwind.source.css @@ -3668,12 +3668,16 @@ display: flex; flex-direction: column; align-items: center; - justify-content: flex-start; + justify-content: center; text-align: center; gap: var(--space-2xs); width: 100%; - padding-block: var(--space-sm); - padding-inline: var(--space-2xs); + /* equal-height tiles: a consistent floor so short and 2-line labels align + (grid rows stretch to the tallest tile, so within a row they match) */ + min-block-size: var(--profile-modal-nav-tile-min-height); + padding-block: var(--space-xs); + padding-inline: var(--space-3xs); + line-height: 1.15; /* wrap labels at spaces, not mid-word */ overflow-wrap: break-word; word-break: normal; @@ -3684,6 +3688,16 @@ .profile-modal__nav > button svg { width: var(--space-lg); height: var(--space-lg); + flex-shrink: 0; + } + + /* In the tile grid the unread dot rides the tile's top-right corner instead + of the vertical-nav left edge (where it landed in the grid gutter). */ + .profile-modal__nav > button .profile-nav__unread-dot { + top: var(--space-2xs); + left: auto; + right: var(--space-2xs); + transform: none; } } diff --git a/css/tokens.css b/css/tokens.css index 239f8286..9551feda 100644 --- a/css/tokens.css +++ b/css/tokens.css @@ -359,6 +359,8 @@ --notification-banner-max-inline: var(--size-28rem); --profile-modal-nav-min-width: max-content; --profile-modal-nav-max-width: max-content; + /* Equal-height floor for the mobile tab-grid tiles. */ + --profile-modal-nav-tile-min-height: 4.75rem; --profile-modal-scrollbar-offset: var(--space-2xs); --modal-embed-height: 80vh; --profile-history-max-height: 26.25rem; diff --git a/js/ui/onboarding/tourEngine.js b/js/ui/onboarding/tourEngine.js index 48373890..7278989f 100644 --- a/js/ui/onboarding/tourEngine.js +++ b/js/ui/onboarding/tourEngine.js @@ -57,6 +57,20 @@ const TOUR_CSS = ` padding: 1rem; transition: left 0.28s ease, top 0.28s ease; } +/* Mobile: dock the popover as a bottom sheet instead of anchoring it to the + target. Anchored placement is fragile on phones (short viewports, wrapping + buttons, URL-bar resize) — a fixed sheet is always in a predictable spot and + the spotlight still highlights the (centered-scrolled) target above it. */ +.bv-tour-popover.bv-tour-popover--sheet { + left: 0.75rem; + right: 0.75rem; + top: auto; + bottom: max(0.75rem, env(safe-area-inset-bottom, 0px)); + transform: none; + width: auto; + max-width: 32rem; + margin-inline: auto; +} .bv-tour-popover h3 { font-size: 1rem; font-weight: 700; margin: 0 0 0.375rem; } .bv-tour-popover p { font-size: 0.875rem; margin: 0 0 0.75rem; opacity: 0.9; } .bv-tour-dots { display: flex; gap: 0.375rem; margin-bottom: 0.75rem; } @@ -171,10 +185,13 @@ export function createTour({ } }; + const isMobileViewport = () => (doc.defaultView?.innerWidth || 1200) < 640; + function measure(step) { const target = step.target ? doc.querySelector(step.target) : null; if (!target) { // Centered card: zero-size cutout in the middle (pure dim). + state.popover.classList.remove("bv-tour-popover--sheet"); setVar(state.scrim, "--bv-tour-x", "50vw"); setVar(state.scrim, "--bv-tour-y", "50vh"); setVar(state.scrim, "--bv-tour-w", "0px"); @@ -187,8 +204,13 @@ export function createTour({ return; } + const mobile = isMobileViewport(); try { - target.scrollIntoView({ block: "nearest", inline: "nearest" }); + // Center the target on mobile so it sits above the bottom sheet. + target.scrollIntoView({ + block: mobile ? "center" : "nearest", + inline: "nearest", + }); } catch (error) { // older engines } @@ -200,11 +222,23 @@ export function createTour({ setVar(state.scrim, "--bv-tour-w", `${rect.width + pad * 2}px`); setVar(state.scrim, "--bv-tour-h", `${rect.height + pad * 2}px`); + if (mobile) { + // Bottom sheet — no anchored-placement math to get wrong. + state.popover.classList.add("bv-tour-popover--sheet"); + return; + } + + // Desktop: anchor, but measure the ACTUAL popover so the fit checks are + // accurate (the old 240px/336px guesses pushed tall popovers off-screen). + state.popover.classList.remove("bv-tour-popover--sheet"); + const popRect = state.popover.getBoundingClientRect(); const placementResult = computeTourPlacement({ rect, viewportWidth: doc.defaultView?.innerWidth || 1200, viewportHeight: doc.defaultView?.innerHeight || 800, preferred: step.placement || "", + popWidth: popRect.width || 336, + popHeight: popRect.height || 240, }); setVar(state.popover, "--bv-tour-px", `${placementResult.px}px`); setVar(state.popover, "--bv-tour-py", `${placementResult.py}px`);