Skip to content

Commit f5dcfd6

Browse files
jeremymanningclaude
andcommitted
Fix tutorial modal/panel overlap and z-index stacking on mobile landscape
- Highlight z-index lowered to 9999 (was 10002) so highlighted map container doesn't stack above panels - Landscape modal positioning now detects open panels and constrains maxWidth to avoid overlapping quiz panel (right side) or video panel (left side) and their toggle tabs - When both panels are open and neither side has room, modal centers on top as an overlay - Corrected available-space calculations for both left-side (video) and right-side (quiz) panels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent eaebcd2 commit f5dcfd6

2 files changed

Lines changed: 66 additions & 19 deletions

File tree

src/ui/tutorial.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
/* Highlight ring on targeted elements */
1717
.tutorial-highlight {
1818
position: relative;
19-
z-index: 10002 !important;
19+
z-index: 9999 !important;
2020
outline: 2px solid var(--tutorial-accent);
2121
outline-offset: 4px;
2222
border-radius: inherit;
2323
box-shadow: 0 0 0 9999px rgba(0,0,0,0.45), 0 0 8px 2px rgba(0, 105, 62, 0.4);
2424
animation: tutorialPulse 1.5s ease-in-out infinite;
2525
}
2626

27-
/* During tutorial, raise drawers and pulls above the tutorial modal (z-index 9999) */
27+
/* During tutorial, raise drawers and pulls above the tutorial modal and highlights */
2828
body.tutorial-active #quiz-panel,
2929
body.tutorial-active #video-panel {
3030
z-index: 10000 !important;

src/ui/tutorial.js

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,26 +1123,73 @@ function renderOverlay(highlightSelector, title, message, showNextBtn, isFinish,
11231123
if (mobile) {
11241124
const isLandscape = window.matchMedia('(orientation: landscape)').matches;
11251125
if (isLandscape) {
1126-
// Landscape: position modal under header, left or right based on positionHint
1126+
// Landscape: position modal under header, avoiding any open panel
11271127
const headerH = document.getElementById('app-header')?.offsetHeight || 48;
11281128
const topPos = (headerH + 8) + 'px';
1129-
const isRightSide = positionHint === 'right' || positionHint === 'video-final';
1130-
const isLeftSide = positionHint === 'left' || positionHint === 'quiz-final';
1131-
Object.assign(modal.style, {
1132-
maxWidth: `${MODAL_MAX_WIDTH}px`, borderRadius: '12px',
1133-
top: topPos,
1134-
});
1135-
if (isRightSide) {
1136-
modal.style.right = '12px';
1137-
modal.style.left = 'auto';
1138-
} else if (isLeftSide) {
1139-
modal.style.left = '12px';
1140-
modal.style.right = 'auto';
1129+
1130+
// Detect which panels are open and compute available space
1131+
const quizPanel = document.getElementById('quiz-panel');
1132+
const videoPanel = document.getElementById('video-panel');
1133+
const quizOpen = quizPanel?.classList.contains('open');
1134+
const videoOpen = videoPanel?.classList.contains('open');
1135+
const toggleWidth = 36; // drawer toggle tab extends past panel edge
1136+
1137+
// Determine best side: prefer positionHint, then place away from open panels
1138+
const hintRight = positionHint === 'right' || positionHint === 'video-final';
1139+
const hintLeft = positionHint === 'left' || positionHint === 'quiz-final';
1140+
let placeLeft = hintLeft;
1141+
let placeRight = hintRight;
1142+
if (!placeLeft && !placeRight) {
1143+
// No hint: place opposite the open panel, or left by default
1144+
placeLeft = !videoOpen;
1145+
placeRight = videoOpen && !quizOpen;
1146+
}
1147+
1148+
// Calculate available width on each side (accounting for panel + toggle)
1149+
let availLeft = window.innerWidth;
1150+
let availRight = window.innerWidth;
1151+
if (quizOpen && quizPanel) {
1152+
// Quiz panel is on the RIGHT — constrains left space and right space
1153+
const qpRect = quizPanel.getBoundingClientRect();
1154+
availLeft = Math.min(availLeft, qpRect.left - toggleWidth);
1155+
availRight = Math.min(availRight, window.innerWidth - qpRect.left);
1156+
}
1157+
if (videoOpen && videoPanel) {
1158+
// Video panel is on the LEFT — constrains right space and left space
1159+
const vpRect = videoPanel.getBoundingClientRect();
1160+
availRight = Math.min(availRight, window.innerWidth - vpRect.right - toggleWidth);
1161+
availLeft = Math.min(availLeft, vpRect.right);
1162+
}
1163+
1164+
// If preferred side has no room, flip; if neither side has room, overlay on top
1165+
const minUsable = 160;
1166+
if (placeLeft && availLeft < minUsable && availRight > availLeft) {
1167+
placeLeft = false; placeRight = true;
1168+
} else if (placeRight && availRight < minUsable && availLeft > availRight) {
1169+
placeRight = false; placeLeft = true;
1170+
}
1171+
1172+
const chosenAvail = placeLeft ? availLeft : availRight;
1173+
if (chosenAvail < minUsable) {
1174+
// Both sides blocked (both panels open) — center overlay on top
1175+
Object.assign(modal.style, {
1176+
maxWidth: `${MODAL_MAX_WIDTH}px`, borderRadius: '12px',
1177+
top: topPos, left: '50%', right: 'auto',
1178+
transform: 'translateX(-50%)',
1179+
});
11411180
} else {
1142-
// Default: center
1143-
modal.style.left = '50%';
1144-
modal.style.right = 'auto';
1145-
modal.style.transform = 'translateX(-50%)';
1181+
const maxW = Math.min(MODAL_MAX_WIDTH, chosenAvail - 24);
1182+
Object.assign(modal.style, {
1183+
maxWidth: Math.max(maxW, minUsable) + 'px', borderRadius: '12px',
1184+
top: topPos,
1185+
});
1186+
if (placeRight) {
1187+
modal.style.right = '12px';
1188+
modal.style.left = 'auto';
1189+
} else {
1190+
modal.style.left = '12px';
1191+
modal.style.right = 'auto';
1192+
}
11461193
}
11471194
} else {
11481195
// Portrait: bottom sheet or top bar

0 commit comments

Comments
 (0)